package prompt

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestParseJudgeJSON_BareObject(t *testing.T) {
	r, err := parseJudgeJSON(`{"kind":"implement.new-feature","confidence":0.82,"target_task_id":"","rationale":"new functionality requested"}`, nil)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindImplementNew {
		t.Errorf("kind = %s, want %s", r.Kind, KindImplementNew)
	}
	if r.Confidence != 0.82 {
		t.Errorf("confidence = %v, want 0.82", r.Confidence)
	}
}

func TestParseJudgeJSON_StripsMarkdownFence(t *testing.T) {
	in := "Here is my classification:\n```json\n{\"kind\":\"debug.discovery\",\"confidence\":0.7,\"target_task_id\":\"\",\"rationale\":\"symptom report\"}\n```\nThanks!"
	r, err := parseJudgeJSON(in, nil)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindDebugDiscovery {
		t.Errorf("kind = %s, want %s", r.Kind, KindDebugDiscovery)
	}
}

func TestParseJudgeJSON_RejectsUnknownKind(t *testing.T) {
	_, err := parseJudgeJSON(`{"kind":"not-a-real-kind","confidence":0.9}`, nil)
	if err == nil {
		t.Fatal("expected error for unknown kind")
	}
	if !strings.Contains(err.Error(), "unknown kind") {
		t.Errorf("err = %v, want 'unknown kind'", err)
	}
}

func TestParseJudgeJSON_SteeringWithoutValidTargetDemoted(t *testing.T) {
	// Judge claims steering but the task id doesn't match an open one — we
	// must NOT pass that through as steering or the master will try to
	// dispatch to a nonexistent task.
	open := []TaskSummary{{ID: "task_real", Title: "x", State: "in_progress"}}
	r, err := parseJudgeJSON(
		`{"kind":"steering","confidence":0.9,"target_task_id":"task_made_up","rationale":"redirect"}`,
		open,
	)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindUnknown {
		t.Errorf("kind = %s, want %s (steering must be demoted when target invalid)", r.Kind, KindUnknown)
	}
}

func TestParseJudgeJSON_SteeringWithValidTargetPassesThrough(t *testing.T) {
	open := []TaskSummary{{ID: "task_real", Title: "x", State: "in_progress"}}
	r, err := parseJudgeJSON(
		`{"kind":"steering","confidence":0.9,"target_task_id":"task_real","rationale":"redirect"}`,
		open,
	)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindSteering || r.TargetTaskID != "task_real" {
		t.Errorf("got %+v, want steering→task_real", r)
	}
}

func TestParseJudgeJSON_ClampsConfidence(t *testing.T) {
	r, _ := parseJudgeJSON(`{"kind":"conversational","confidence":1.7,"target_task_id":"","rationale":""}`, nil)
	if r.Confidence != 1.0 {
		t.Errorf("confidence = %v, want clamped to 1.0", r.Confidence)
	}
	r, _ = parseJudgeJSON(`{"kind":"conversational","confidence":-0.3,"target_task_id":"","rationale":""}`, nil)
	if r.Confidence != 0 {
		t.Errorf("confidence = %v, want clamped to 0", r.Confidence)
	}
}

func TestExtractJSONObject_HandlesBracesInStrings(t *testing.T) {
	// A naive brace-counter would over-close at the } inside "rationale".
	in := `preamble {"kind":"unknown","confidence":0.5,"target_task_id":"","rationale":"the user typed { and }"} trailing`
	got := extractJSONObject(in)
	want := `{"kind":"unknown","confidence":0.5,"target_task_id":"","rationale":"the user typed { and }"}`
	if got != want {
		t.Errorf("got %q\nwant %q", got, want)
	}
}

func TestExtractJSONObject_HandlesEscapedQuotes(t *testing.T) {
	in := `{"rationale":"he said \"hi\" then {"} junk`
	got := extractJSONObject(in)
	want := `{"rationale":"he said \"hi\" then {"}`
	if got != want {
		t.Errorf("got %q\nwant %q", got, want)
	}
}

// stubRoundTripper lets us simulate the Anthropic API without hitting the network.
type stubRoundTripper struct {
	resp *http.Response
	err  error
}

func (s *stubRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
	return s.resp, s.err
}

func TestLLMJudge_FallsBackOnHTTPError(t *testing.T) {
	// HTTP error → must fall back to Rules so the master still gets routed.
	j := NewLLMJudge("dummy-key", NewRules())
	j.HTTP = &http.Client{Transport: &stubRoundTripper{err: errors.New("network down")}}

	r, err := j.Classify(context.Background(), Input{Text: "add a tooltip to the dashboard"})
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindImplementNew {
		t.Errorf("kind = %s, want %s (rules fallback should match 'add')", r.Kind, KindImplementNew)
	}
	if !strings.Contains(r.Rationale, "fallback") {
		t.Errorf("rationale should mark the fallback path; got %q", r.Rationale)
	}
}

func TestLLMJudge_UsesLLMResponseOnSuccess(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"content":[{"type":"text","text":"{\"kind\":\"debug.discovery\",\"confidence\":0.88,\"target_task_id\":\"\",\"rationale\":\"user reports something is wrong\"}"}]}`))
	}))
	defer server.Close()

	j := NewLLMJudge("dummy-key", NewRules())
	j.Endpoint = server.URL

	// A prompt with no debug keywords ("crashes"/"broken"/etc.) — Rules
	// would return Unknown 0.20, but the stubbed LLM returns debug. We
	// must see the LLM's answer, not the rules fallback.
	r, err := j.Classify(context.Background(), Input{Text: "the new design is doing something I didn't expect"})
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindDebugDiscovery {
		t.Errorf("kind = %s, want %s (LLM verdict)", r.Kind, KindDebugDiscovery)
	}
	if r.Confidence != 0.88 {
		t.Errorf("confidence = %v, want 0.88 from LLM", r.Confidence)
	}
}

func TestLLMJudge_FallsBackOnMalformedLLMOutput(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"content":[{"type":"text","text":"I'm not going to give you JSON, sorry."}]}`))
	}))
	defer server.Close()

	j := NewLLMJudge("dummy-key", NewRules())
	j.Endpoint = server.URL

	r, err := j.Classify(context.Background(), Input{Text: "rename foo to bar"})
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if r.Kind != KindImplementCrossBound {
		t.Errorf("kind = %s, want %s (rules fallback should match 'rename')", r.Kind, KindImplementCrossBound)
	}
}

func TestNewClassifierFromEnv_RulesOverride(t *testing.T) {
	t.Setenv("HARNESS_CLASSIFIER", "rules")
	t.Setenv("ANTHROPIC_API_KEY", "would-be-used")
	c := NewClassifierFromEnv()
	if _, ok := c.(*Rules); !ok {
		t.Errorf("HARNESS_CLASSIFIER=rules must return *Rules, got %T", c)
	}
}

func TestNewClassifierFromEnv_DefaultsToRulesWithoutKey(t *testing.T) {
	t.Setenv("HARNESS_CLASSIFIER", "")
	t.Setenv("ANTHROPIC_API_KEY", "")
	c := NewClassifierFromEnv()
	if _, ok := c.(*Rules); !ok {
		t.Errorf("no key + no override should yield *Rules, got %T", c)
	}
}

func TestNewClassifierFromEnv_DefaultsToLLMWhenKeyPresent(t *testing.T) {
	t.Setenv("HARNESS_CLASSIFIER", "")
	t.Setenv("ANTHROPIC_API_KEY", "sk-test")
	c := NewClassifierFromEnv()
	if _, ok := c.(*LLMJudge); !ok {
		t.Errorf("API key present should yield *LLMJudge, got %T", c)
	}
}
