package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

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

	"git.agentview.dev/profit/golangLAKEHOUSE/internal/validator"
)

// readSessionLog parses the JSONL session log into row maps. Used by
// the iterate-handler tests to verify per-session entries.
func readSessionLog(t *testing.T, path string) []map[string]any {
	t.Helper()
	f, err := os.Open(path)
	if err != nil {
		t.Fatalf("open session log: %v", err)
	}
	defer f.Close()
	var out []map[string]any
	sc := bufio.NewScanner(f)
	sc.Buffer(make([]byte, 0, 1<<16), 1<<24)
	for sc.Scan() {
		line := sc.Bytes()
		if len(line) == 0 {
			continue
		}
		var row map[string]any
		if err := json.Unmarshal(line, &row); err != nil {
			t.Fatalf("parse session log line: %v", err)
		}
		out = append(out, row)
	}
	return out
}

// newTestRouter builds the validatord router with an explicit lookup
// + a fake chatd URL. Tests that exercise /iterate need a live mock
// chatd (constructed inline per-test).
func newTestRouter(lookup validator.WorkerLookup, chatdURL string) http.Handler {
	h := newTestHandlers(lookup, chatdURL, nil)
	r := chi.NewRouter()
	h.register(r)
	return r
}

// newTestHandlers exposes the underlying handlers struct so tests can
// inspect post-call state (e.g. session log contents). sessionLog is
// optional — pass nil to disable the session log.
func newTestHandlers(lookup validator.WorkerLookup, chatdURL string, sessionLog *validator.SessionLogger) *handlers {
	return &handlers{
		lookup:     lookup,
		chatdURL:   chatdURL,
		chatClient: &http.Client{Timeout: 5 * time.Second},
		iterCfg: validator.IterateConfig{
			DefaultMaxIterations: 3,
			DefaultMaxTokens:     4096,
		},
		sessionLog: sessionLog,
	}
}

// ─── /validate ─────────────────────────────────────────────────

func TestValidate_RejectsUnknownKind(t *testing.T) {
	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), "")
	body := []byte(`{"kind":"unknown","artifact":{}}`)
	req := httptest.NewRequest("POST", "/validate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for unknown kind, got %d (body=%s)", w.Code, w.Body.String())
	}
}

func TestValidate_RejectsMissingArtifact(t *testing.T) {
	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), "")
	body := []byte(`{"kind":"playbook"}`)
	req := httptest.NewRequest("POST", "/validate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for missing artifact, got %d", w.Code)
	}
}

func TestValidate_PlaybookHappyPath(t *testing.T) {
	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), "")
	body := []byte(`{
		"kind": "playbook",
		"artifact": {
			"operation": "fill: Welder x2 in Toledo, OH",
			"endorsed_names": ["W-1","W-2"],
			"target_count": 2,
			"fingerprint": "abc123"
		}
	}`)
	req := httptest.NewRequest("POST", "/validate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d (body=%s)", w.Code, w.Body.String())
	}
	var report validator.Report
	if err := json.Unmarshal(w.Body.Bytes(), &report); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if report.ElapsedMs < 0 {
		t.Errorf("elapsed_ms negative: %d", report.ElapsedMs)
	}
}

func TestValidate_PlaybookSchemaErrorReturns422(t *testing.T) {
	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), "")
	body := []byte(`{
		"kind": "playbook",
		"artifact": {
			"operation": "wrong_prefix: foo",
			"endorsed_names": ["a"],
			"fingerprint": "x"
		}
	}`)
	req := httptest.NewRequest("POST", "/validate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d (body=%s)", w.Code, w.Body.String())
	}
	var ve validator.ValidationError
	if err := json.Unmarshal(w.Body.Bytes(), &ve); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if ve.Kind != validator.ErrSchema {
		t.Errorf("kind = %v, want schema", ve.Kind)
	}
}

func TestValidate_FillRoutesThroughLookup(t *testing.T) {
	city := "Toledo"
	lookup := validator.NewInMemoryWorkerLookup([]validator.WorkerRecord{
		{CandidateID: "W-1", Name: "Ada", Status: "active", City: &city},
	})
	r := newTestRouter(lookup, "")

	// Candidate that doesn't exist in lookup → consistency failure.
	body := []byte(`{
		"kind": "fill",
		"artifact": {
			"fills": [{"candidate_id":"W-PHANTOM","name":"Nobody"}]
		},
		"context": {"target_count": 1, "city": "Toledo", "client_id": "C-1"}
	}`)
	req := httptest.NewRequest("POST", "/validate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422 for phantom candidate, got %d (body=%s)", w.Code, w.Body.String())
	}
}

func TestValidate_ContextMergedIntoArtifactContext(t *testing.T) {
	// _context.target_count from the request `context` block must
	// reach the FillValidator's completeness check. Without the
	// merge, target_count would default to 0 and any non-empty fills
	// list would fail Completeness.
	city := "Toledo"
	role := "Welder"
	lookup := validator.NewInMemoryWorkerLookup([]validator.WorkerRecord{
		{CandidateID: "W-1", Name: "Ada", Status: "active", City: &city, Role: &role},
	})
	r := newTestRouter(lookup, "")
	body := []byte(`{
		"kind": "fill",
		"artifact": {"fills":[{"candidate_id":"W-1","name":"Ada"}]},
		"context": {"target_count": 1, "city": "Toledo", "role": "Welder", "client_id": "C-1"}
	}`)
	req := httptest.NewRequest("POST", "/validate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200 with context merged, got %d (body=%s)", w.Code, w.Body.String())
	}
}

// ─── /iterate ──────────────────────────────────────────────────

// fakeChatd returns a stand-in chatd HTTP server that emits the given
// content string for every /chat call. Caller closes the server.
func fakeChatd(t *testing.T, content string) *httptest.Server {
	t.Helper()
	mux := chi.NewRouter()
	mux.Post("/chat", func(w http.ResponseWriter, _ *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"model":     "test-model",
			"content":   content,
			"provider":  "test",
			"latency_ms": 1,
		})
	})
	return httptest.NewServer(mux)
}

func TestIterate_RejectsMissingFields(t *testing.T) {
	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), "")
	body := []byte(`{"kind":"playbook","prompt":"x"}`) // missing provider+model
	req := httptest.NewRequest("POST", "/iterate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d", w.Code)
	}
}

func TestIterate_HappyPath_ReturnsAcceptedArtifact(t *testing.T) {
	server := fakeChatd(t, `{"operation":"fill: Welder x1 in Toledo, OH","endorsed_names":["W-1"],"target_count":1,"fingerprint":"abc"}`)
	defer server.Close()

	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), server.URL)
	body, _ := json.Marshal(map[string]any{
		"kind":     "playbook",
		"prompt":   "produce a playbook artifact",
		"provider": "ollama",
		"model":    "qwen3.5:latest",
	})
	req := httptest.NewRequest("POST", "/iterate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d (body=%s)", w.Code, w.Body.String())
	}
	var resp validator.IterateResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Iterations != 1 {
		t.Errorf("iterations = %d, want 1", resp.Iterations)
	}
	if resp.Artifact["operation"] != "fill: Welder x1 in Toledo, OH" {
		t.Errorf("artifact.operation: %v", resp.Artifact["operation"])
	}
}

func TestIterate_MaxIterReturns422WithHistory(t *testing.T) {
	// Always returns a no-JSON response, so iterate exhausts retries.
	server := fakeChatd(t, "no json here, just prose")
	defer server.Close()

	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), server.URL)
	body, _ := json.Marshal(map[string]any{
		"kind":           "playbook",
		"prompt":         "produce X",
		"provider":       "ollama",
		"model":          "x",
		"max_iterations": 2,
	})
	req := httptest.NewRequest("POST", "/iterate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d (body=%s)", w.Code, w.Body.String())
	}
	var fail validator.IterateFailure
	if err := json.Unmarshal(w.Body.Bytes(), &fail); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if fail.Iterations != 2 {
		t.Errorf("iterations = %d, want 2", fail.Iterations)
	}
	for _, h := range fail.History {
		if h.Status.Kind != "no_json" {
			t.Errorf("expected all attempts to be no_json, got %v", h.Status.Kind)
		}
	}
}

// TestIterate_WritesSessionRowOnSuccess locks the offline-analysis
// contract: every successful /iterate call appends one SessionRecord
// to the configured JSONL with grounded_in_roster=true (because the
// model emitted real candidate IDs that exist in the roster).
func TestIterate_WritesSessionRowOnSuccess(t *testing.T) {
	city, role := "Toledo", "Welder"
	lookup := validator.NewInMemoryWorkerLookup([]validator.WorkerRecord{
		{CandidateID: "W-1", Name: "Ada", Status: "active", City: &city, Role: &role},
	})
	server := fakeChatd(t, `{"fills":[{"candidate_id":"W-1","name":"Ada"}]}`)
	defer server.Close()

	dir := t.TempDir()
	logPath := filepath.Join(dir, "sessions.jsonl")
	h := newTestHandlers(lookup, server.URL, validator.NewSessionLogger(logPath))
	r := chi.NewRouter()
	h.register(r)

	body, _ := json.Marshal(map[string]any{
		"kind":     "fill",
		"prompt":   "produce fill",
		"provider": "ollama",
		"model":    "qwen3.5:latest",
		"context":  map[string]any{"target_count": 1, "city": "Toledo", "role": "Welder", "client_id": "C-1"},
	})
	req := httptest.NewRequest("POST", "/iterate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d (body=%s)", w.Code, w.Body.String())
	}

	rows := readSessionLog(t, logPath)
	if len(rows) != 1 {
		t.Fatalf("expected 1 session row, got %d", len(rows))
	}
	row := rows[0]
	if row["schema"] != validator.SessionRecordSchema {
		t.Errorf("schema: %v", row["schema"])
	}
	if row["final_verdict"] != "accepted" {
		t.Errorf("final_verdict: %v", row["final_verdict"])
	}
	if row["grounded_in_roster"] != true {
		t.Errorf("grounded_in_roster: expected true (W-1 is in roster), got %v", row["grounded_in_roster"])
	}
	if row["kind"] != "fill" {
		t.Errorf("kind: %v", row["kind"])
	}
}

// TestIterate_GroundedFalseWhenArtifactReferencesPhantom is the
// load-bearing forensic property J flagged 2026-05-02: phantom
// worker IDs in the final accepted artifact must surface as
// grounded_in_roster=false in the session log. This shouldn't happen
// in practice (FillValidator catches phantoms before they reach this
// row), but the explicit check guards against a future bug where the
// validator weakens or a different validator path skips the roster
// check. Defense-in-depth at the observability layer.
func TestIterate_GroundedFalseWhenArtifactReferencesPhantom(t *testing.T) {
	// Empty roster — every candidate ID is "phantom". Plus we use
	// the playbook kind so the validator doesn't reject the artifact
	// upfront (PlaybookValidator doesn't consult the roster), letting
	// the artifact reach the session log. We then build a custom
	// rosterCheckFor "fill" closure and exercise it directly.
	lookup := validator.NewInMemoryWorkerLookup(nil)
	h := newTestHandlers(lookup, "", nil)
	check := h.rosterCheckFor("fill")
	if check == nil {
		t.Fatal("expected non-nil checker for fill kind")
	}
	got := check(map[string]any{
		"fills": []any{
			map[string]any{"candidate_id": "W-PHANTOM-NEVER-EXISTS"},
		},
	})
	if got == nil || *got {
		t.Errorf("expected grounded=false for phantom candidate, got %v", got)
	}
	// Other kinds: no roster concept → checker returns nil.
	if h.rosterCheckFor("email") != nil {
		t.Error("email kind should not have a roster checker")
	}
	if h.rosterCheckFor("playbook") != nil {
		t.Error("playbook kind should not have a roster checker")
	}
}

// TestIterate_WritesSessionRowOnFailure: max-iter exhaustion still
// produces a row (omits artifact, captures the retry chain).
func TestIterate_WritesSessionRowOnFailure(t *testing.T) {
	server := fakeChatd(t, "no json here, just prose")
	defer server.Close()

	dir := t.TempDir()
	logPath := filepath.Join(dir, "sessions.jsonl")
	h := newTestHandlers(validator.NewInMemoryWorkerLookup(nil), server.URL,
		validator.NewSessionLogger(logPath))
	r := chi.NewRouter()
	h.register(r)

	body, _ := json.Marshal(map[string]any{
		"kind":           "playbook",
		"prompt":         "produce X",
		"provider":       "ollama",
		"model":          "x",
		"max_iterations": 2,
	})
	req := httptest.NewRequest("POST", "/iterate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	if w.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", w.Code)
	}

	rows := readSessionLog(t, logPath)
	if len(rows) != 1 {
		t.Fatalf("expected 1 session row, got %d", len(rows))
	}
	if rows[0]["final_verdict"] != "max_iter_exhausted" {
		t.Errorf("final_verdict: %v", rows[0]["final_verdict"])
	}
	if _, hasArtifact := rows[0]["artifact"]; hasArtifact {
		t.Errorf("artifact should be omitted on failure: %v", rows[0]["artifact"])
	}
}

func TestIterate_ChatdDownReturns502(t *testing.T) {
	r := newTestRouter(validator.NewInMemoryWorkerLookup(nil), "http://127.0.0.1:1") // unroutable
	body, _ := json.Marshal(map[string]any{
		"kind":     "playbook",
		"prompt":   "X",
		"provider": "ollama",
		"model":    "x",
	})
	req := httptest.NewRequest("POST", "/iterate", bytes.NewReader(body))
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	if w.Code != http.StatusBadGateway {
		t.Fatalf("expected 502, got %d (body=%s)", w.Code, w.Body.String())
	}
}
