package testutil

// Package testutil provides test helpers for handler and integration tests.
// It wires up an in-memory SQLite database, a lightweight HTTP request helper,
// and common assertion utilities.

import (
	"Engine/internal/database"
	"bytes"
	"database/sql"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	_ "modernc.org/sqlite" // pure-Go SQLite driver, no CGO required
)

// ── Database ──────────────────────────────────────────────────────────────────

// NewTestDB opens a fresh in-memory SQLite database, runs your schema
// migrations, and registers a t.Cleanup to close it when the test ends.
//
// Each call gets its own isolated database — tests never share state.
func NewTestDB(t *testing.T) *sql.DB {
	t.Helper()

	// "file::memory:?cache=shared" would share across connections;
	// the plain ":memory:" gives a fully isolated DB per open call.
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		t.Fatalf("testutil.NewTestDB: open: %v", err)
	}

	// SQLite in-memory works best with a single connection.
	db.SetMaxOpenConns(1)

	if err := database.Migrate(db); err != nil {
		db.Close()
		t.Fatalf("testutil.NewTestDB: migrate: %v", err)
	}

	t.Cleanup(func() { db.Close() })
	return db
}

// ── HTTP helpers ──────────────────────────────────────────────────────────────

// Do fires an HTTP request directly at handler h and returns the recorded
// response. body is marshalled to JSON; pass nil to send no body.
//
// Usage:
//
//	w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", payload)
func Do(t *testing.T, h http.Handler, method, path string, body any) *httptest.ResponseRecorder {
	t.Helper()

	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("testutil.Do: marshal body: %v", err)
		}
		r = bytes.NewReader(b)
	}

	req := httptest.NewRequest(method, path, r)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

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

// MustJSON marshals v to JSON and returns it as an io.Reader.
// Intended for use with http.Client.Post in full-server integration tests.
//
// Usage:
//
//	resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", testutil.MustJSON(t, payload))
func MustJSON(t *testing.T, v any) io.Reader {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("testutil.MustJSON: %v", err)
	}
	return bytes.NewReader(b)
}

// ── Assertions ────────────────────────────────────────────────────────────────

// AssertStatus fails the test if the recorder's status code does not match want.
func AssertStatus(t *testing.T, w *httptest.ResponseRecorder, want int) {
	t.Helper()
	if w.Code != want {
		t.Errorf("status: got %d, want %d\n\tbody: %s", w.Code, want, w.Body.String())
	}
}

// AssertJSONField decodes the recorder's body as a JSON object and checks that
// the top-level key equals the expected string value. Useful for quick smoke
// checks without defining a full response struct.
//
// Usage:
//
//	testutil.AssertJSONField(t, w, "status", "ok")
func AssertJSONField(t *testing.T, w *httptest.ResponseRecorder, key, want string) {
	t.Helper()
	var m map[string]any
	if err := json.NewDecoder(w.Body).Decode(&m); err != nil {
		t.Fatalf("AssertJSONField: decode body: %v", err)
	}
	got, ok := m[key]
	if !ok {
		t.Errorf("AssertJSONField: key %q not found in response", key)
		return
	}
	if got != want {
		t.Errorf("AssertJSONField: %q = %q, want %q", key, got, want)
	}
}

// DecodeJSON decodes the recorder's body into dst.
// Fails the test immediately if decoding errors.
func DecodeJSON(t *testing.T, w *httptest.ResponseRecorder, dst any) {
	t.Helper()
	if err := json.NewDecoder(w.Body).Decode(dst); err != nil {
		t.Fatalf("DecodeJSON: %v\n\tbody: %s", err, w.Body.String())
	}
}
