//go:build test
// +build test

package testutils

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"crussell/testutils/jwt"
	"crussell/testutils/testdb"

	"github.com/jackc/pgx/v5/pgxpool"
)

// contextKey matches mw.UserIDKey to avoid import cycle with auth package.
type contextKey string

const userIDKey contextKey = "user_id"

// MakeRequest makes an HTTP request to a handler with optional JWT token.
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
func MakeRequest(handler http.Handler, method, path string, body interface{}, token string, ctx context.Context) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}

	req = req.WithContext(ctx)

	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

// MakeRequestWithContext makes an HTTP request with context values (for auth middleware testing)
// Use this when you need to test handlers that rely on context values set by middleware
func MakeRequestWithContext(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}

	req = req.WithContext(ctx)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

// MakeUserRequest makes a request as an authenticated user.
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
func MakeUserRequest(handler http.Handler, method, path string, body interface{}, userID string, ctx context.Context) *httptest.ResponseRecorder {
	token := jwt.GenerateUserToken(userID)
	return MakeRequest(handler, method, path, body, token, ctx)
}

// MakeAdminRequest makes a request as an authenticated admin.
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
func MakeAdminRequest(handler http.Handler, method, path string, body interface{}, adminID string, ctx context.Context) *httptest.ResponseRecorder {
	token := jwt.GenerateTestToken(adminID, "admin")
	return MakeRequest(handler, method, path, body, token, ctx)
}

// MakeContextRequest makes a request with user context set.
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
// The user ID is layered on top of the transaction context.
func MakeContextRequest(handler http.Handler, method, path string, body interface{}, userID string, ctx context.Context) *httptest.ResponseRecorder {
	reqCtx := context.WithValue(ctx, userIDKey, userID)
	return MakeRequestWithContext(handler, method, path, body, reqCtx)
}

// ParseResponseBody unmarshals the response body into dest
func ParseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
	return json.Unmarshal(w.Body.Bytes(), dest)
}

// AssertStatusCode checks that the response has the expected HTTP status code
func AssertStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedCode int) {
	t.Helper()
	if w.Code != expectedCode {
		t.Errorf("expected status %d, got %d. body: %s", expectedCode, w.Code, w.Body.String())
	}
}

// AssertStatusCodeWithMessage checks status code and logs full response on mismatch
func AssertStatusCodeWithMessage(t *testing.T, w *httptest.ResponseRecorder, expectedCode int, message string) {
	t.Helper()
	if w.Code != expectedCode {
		t.Errorf("%s: expected status %d, got %d.\nResponse: %s", message, expectedCode, w.Code, w.Body.String())
	}
}

// AssertJSONResponse checks that the response is valid JSON and unmarshals it
func AssertJSONResponse(t *testing.T, w *httptest.ResponseRecorder, dest interface{}) {
	t.Helper()
	if err := ParseResponseBody(w, dest); err != nil {
		t.Errorf("failed to parse JSON response: %v\nBody: %s", err, w.Body.String())
	}
}

// AssertResponseContains checks that the response body contains a substring
func AssertResponseContains(t *testing.T, w *httptest.ResponseRecorder, substring string) {
	t.Helper()
	if !bytes.Contains(w.Body.Bytes(), []byte(substring)) {
		t.Errorf("expected response to contain '%s', but got:\n%s", substring, w.Body.String())
	}
}

// AssertResponseNotContains checks that the response body does NOT contain a substring
func AssertResponseNotContains(t *testing.T, w *httptest.ResponseRecorder, substring string) {
	t.Helper()
	if bytes.Contains(w.Body.Bytes(), []byte(substring)) {
		t.Errorf("expected response to NOT contain '%s', but got:\n%s", substring, w.Body.String())
	}
}

// TestDBSnapshot creates a snapshot of the test database for transaction rollback
// Returns the pool and a cleanup function
func TestDBSnapshot(t *testing.T) (*pgxpool.Pool, func()) {
	t.Helper()
	pool := testdb.Pool(t)
	testdb.Migrate(t, pool)
	return pool, func() { pool.Close() }
}

// GetBodyAsString returns the response body as a string
func GetBodyAsString(w *httptest.ResponseRecorder) string {
	return w.Body.String()
}

// GetBodyAsJSON unmarshals and returns the response body
// Returns error if JSON is invalid
func GetBodyAsJSON(w *httptest.ResponseRecorder) (map[string]interface{}, error) {
	var result map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &result)
	return result, err
}

// MakeRequestNoAuth makes an HTTP request without authentication.
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
func MakeRequestNoAuth(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
	return MakeRequest(handler, method, path, body, "", ctx)
}

// AssertErrorStatusCode checks status code and validates error is in response body
func AssertErrorStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedCode int, expectedErrorSubstring string) {
	t.Helper()
	AssertStatusCode(t, w, expectedCode)
	AssertResponseContains(t, w, expectedErrorSubstring)
}

// GetResponseStatus returns just the status code for convenient assertions
func GetResponseStatus(w *httptest.ResponseRecorder) int {
	return w.Code
}
