package main

import (
	"bytes"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-chi/chi/v5"
)

// Closes R-005 for queryd: cmd-level tests for the /sql handler's
// pre-DB paths (decode, body cap, empty SQL). The actual SQL execution
// path needs DuckDB so it lives in the smoke chain + proof harness.
//
// We construct handlers with a nil *sql.DB — the tests only exercise
// paths that return early before db.QueryContext. Tests that would
// reach the db are covered by GOLAKE-040 in the proof harness.

func mountedRouter() chi.Router {
	h := &handlers{db: nil}
	r := chi.NewRouter()
	h.register(r)
	return r
}

func TestRoutesMounted(t *testing.T) {
	r := mountedRouter()
	found := false
	chi.Walk(r, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
		if method == "POST" && route == "/sql" {
			found = true
		}
		return nil
	})
	if !found {
		t.Error("POST /sql not mounted")
	}
}

func TestHandleSQL_BodyTooLarge(t *testing.T) {
	// 4xx range — see embedd's TestHandleEmbed_BodyTooLarge for the
	// 413-vs-400 detail. The contract is "client error, fails loud."
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	big := bytes.Repeat([]byte("x"), maxSQLBodyBytes+1024)
	resp, err := http.Post(srv.URL+"/sql", "application/json", bytes.NewReader(big))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 400 || resp.StatusCode >= 500 {
		t.Errorf("expected 4xx on oversize, got %d", resp.StatusCode)
	}
}

func TestHandleSQL_MalformedJSON_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/sql", "application/json", strings.NewReader("not json"))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on malformed, got %d", resp.StatusCode)
	}
}

// TestHandleSQL_WrongFieldName_400 locks the JSON tag on sqlRequest.SQL
// against drift. The 2026-04-30 playbook_lift harness sent {"q": "..."}
// — the Go decoder ignores unknown fields by default, so req.SQL stays
// empty and the empty-check fires with "sql is empty". If anyone renames
// the JSON tag, callers POSTing the new (wrong) shape would hit this
// same path; this test makes the contract explicit so the failure mode
// is documented rather than discovered during a reality run.
func TestHandleSQL_WrongFieldName_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	cases := []string{
		`{"q":"SELECT 1"}`,        // the actual 2026-04-30 harness shape
		`{"query":"SELECT 1"}`,    // matrixd-style drift in the other direction
		`{"statement":"SELECT 1"}`,
	}
	for _, body := range cases {
		t.Run(body, func(t *testing.T) {
			resp, err := http.Post(srv.URL+"/sql", "application/json", strings.NewReader(body))
			if err != nil {
				t.Fatalf("POST: %v", err)
			}
			defer resp.Body.Close()
			if resp.StatusCode != http.StatusBadRequest {
				t.Errorf("expected 400 on wrong field name, got %d", resp.StatusCode)
			}
			rb, _ := io.ReadAll(resp.Body)
			if !strings.Contains(string(rb), "sql is empty") {
				t.Errorf("expected 'sql is empty' to anchor the contract, got %q", string(rb))
			}
		})
	}
}

func TestHandleSQL_EmptySQL_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	cases := []string{
		`{"sql":""}`,
		`{"sql":"   "}`,
		`{"sql":"\n\t  \n"}`,
	}
	for _, body := range cases {
		t.Run(body, func(t *testing.T) {
			resp, err := http.Post(srv.URL+"/sql", "application/json", strings.NewReader(body))
			if err != nil {
				t.Fatalf("POST: %v", err)
			}
			defer resp.Body.Close()
			if resp.StatusCode != http.StatusBadRequest {
				t.Errorf("expected 400 on empty/whitespace SQL, got %d", resp.StatusCode)
			}
		})
	}
}

func TestMaxSQLBodyBytes_Reasonable(t *testing.T) {
	// SQL strings shouldn't be huge — 64 KiB is generous for queryd
	// (DuckDB statements above 64 KiB are pathological). Locking the
	// constant prevents an accidental refactor from blowing this open.
	if maxSQLBodyBytes < 16<<10 {
		t.Errorf("maxSQLBodyBytes=%d below sane SQL minimum (16 KiB)", maxSQLBodyBytes)
	}
	if maxSQLBodyBytes > 1<<20 {
		t.Errorf("maxSQLBodyBytes=%d above sane SQL maximum (1 MiB)", maxSQLBodyBytes)
	}
}

func TestPrimaryBucket_Constant(t *testing.T) {
	// Locks the logical bucket name — secrets provider lookup keys
	// against this. Refactor that flips this would silently fail
	// secret resolution for queryd at startup.
	if primaryBucket != "primary" {
		t.Errorf("primaryBucket = %q, want %q", primaryBucket, "primary")
	}
}
