package chat

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// openaiServer returns an httptest server that mocks an OpenAI Chat
// Completions endpoint. Handler captures the last request body for
// assertion.
func openaiServer(t *testing.T, status int, respBody string) (*httptest.Server, *string) {
	t.Helper()
	captured := ""
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		bs, _ := io.ReadAll(r.Body)
		captured = string(bs)
		// Ensure Authorization header was set (Bearer prefix).
		if auth := r.Header.Get("Authorization"); !strings.HasPrefix(auth, "Bearer ") {
			t.Errorf("missing Bearer auth header; got %q", auth)
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		_, _ = w.Write([]byte(respBody))
	}))
	t.Cleanup(srv.Close)
	return srv, &captured
}

func TestOpenAICompat_HappyPath(t *testing.T) {
	resp := `{
		"model": "anthropic/claude-opus-4-7",
		"choices": [{"message":{"content":"hello world"}, "finish_reason":"stop"}],
		"usage": {"prompt_tokens": 10, "completion_tokens": 5}
	}`
	srv, captured := openaiServer(t, 200, resp)

	p := newOpenAICompat("openrouter", srv.URL+"/v1", "test-key", "openrouter", 5*time.Second)
	out, err := p.Chat(context.Background(), Request{
		Model:    "openrouter/anthropic/claude-opus-4-7",
		Messages: []Message{{Role: "user", Content: "hi"}},
	})
	if err != nil {
		t.Fatalf("Chat: %v", err)
	}
	if out.Content != "hello world" {
		t.Errorf("Content = %q, want hello world", out.Content)
	}
	// Provider strips its prefix before sending to upstream.
	var sent map[string]any
	if err := json.Unmarshal([]byte(*captured), &sent); err != nil {
		t.Fatalf("parse captured: %v", err)
	}
	if sent["model"] != "anthropic/claude-opus-4-7" {
		t.Errorf("upstream got model = %v, want anthropic/claude-opus-4-7 (prefix stripped)", sent["model"])
	}
	// Token accounting carried through.
	if out.InputTokens != 10 || out.OutputTokens != 5 {
		t.Errorf("tokens = (%d, %d), want (10, 5)", out.InputTokens, out.OutputTokens)
	}
	if out.FinishReason != "stop" {
		t.Errorf("FinishReason = %q, want stop", out.FinishReason)
	}
}

func TestOpenAICompat_FormatJSON(t *testing.T) {
	resp := `{"choices":[{"message":{"content":"{\"a\":1}"},"finish_reason":"stop"}],"usage":{}}`
	srv, captured := openaiServer(t, 200, resp)
	p := newOpenAICompat("opencode", srv.URL+"/zen/v1", "test-key", "opencode", 5*time.Second)

	_, err := p.Chat(context.Background(), Request{
		Model:    "opencode/claude-opus-4-7",
		Messages: []Message{{Role: "user", Content: "give me JSON"}},
		Format:   "json",
	})
	if err != nil {
		t.Fatalf("Chat: %v", err)
	}
	// Format=json should set response_format on the upstream call.
	if !strings.Contains(*captured, `"response_format"`) || !strings.Contains(*captured, `"json_object"`) {
		t.Errorf("Format=json should set response_format json_object; captured=%s", *captured)
	}
}

func TestOpenAICompat_UpstreamError(t *testing.T) {
	srv, _ := openaiServer(t, 429, `{"error":"rate limited"}`)
	p := newOpenAICompat("openrouter", srv.URL+"/v1", "test-key", "openrouter", 5*time.Second)
	_, err := p.Chat(context.Background(), Request{Model: "openrouter/x"})
	if !errors.Is(err, ErrUpstream) {
		t.Errorf("429 should be ErrUpstream; got %v", err)
	}
}

func TestOpenAICompat_ZeroChoices(t *testing.T) {
	srv, _ := openaiServer(t, 200, `{"choices":[],"usage":{}}`)
	p := newOpenAICompat("openrouter", srv.URL+"/v1", "test-key", "openrouter", 5*time.Second)
	_, err := p.Chat(context.Background(), Request{Model: "openrouter/x"})
	if !errors.Is(err, ErrUpstream) {
		t.Errorf("zero-choices should ErrUpstream; got %v", err)
	}
}

func TestOpenAICompat_EmptyKeyUnavailable(t *testing.T) {
	p := newOpenAICompat("openrouter", "https://example.com", "", "openrouter", 5*time.Second)
	if p.Available() {
		t.Errorf("empty key should make provider unavailable")
	}
}

// TestOllamaCloud_StripsCorrectPrefix locks in scrum fix B-3.
// Pre-fix: StripPrefix(req.Model, "cloud") never matched "ollama_cloud/<m>"
// because the prefix string was "cloud/" not "ollama_cloud/". Result was
// the upstream got the prefixed model name and 400'd. This test would
// have caught it had it existed.
func TestOllamaCloud_StripsCorrectPrefix(t *testing.T) {
	captured := ""
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		bs, _ := io.ReadAll(r.Body)
		captured = string(bs)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(200)
		_, _ = w.Write([]byte(`{"model":"deepseek-v3.2","response":"ok","done":true,"done_reason":"stop"}`))
	}))
	t.Cleanup(srv.Close)

	// Inject the test server URL via reflection-free constructor —
	// just build the struct directly with our overrides since
	// NewOllamaCloud hardcodes ollama.com.
	o := &OllamaCloud{
		apiKey:     "test-key",
		baseURL:    srv.URL,
		httpClient: srv.Client(),
	}

	cases := []struct {
		input    string
		expected string // model the upstream should see
	}{
		// Prefix form (Phase 4 scrum fix B-3): strip "ollama_cloud/"
		{"ollama_cloud/deepseek-v3.2", "deepseek-v3.2"},
		{"ollama_cloud/kimi-k2.6:cloud", "kimi-k2.6:cloud"},
		// Suffix form: nothing to strip (already canonical)
		{"kimi-k2.6:cloud", "kimi-k2.6:cloud"},
		{"qwen3-coder:480b", "qwen3-coder:480b"},
	}
	for _, c := range cases {
		captured = ""
		_, err := o.Chat(context.Background(), Request{
			Model:    c.input,
			Messages: []Message{{Role: "user", Content: "hi"}},
		})
		if err != nil {
			t.Errorf("Chat(%q): %v", c.input, err)
			continue
		}
		var sent map[string]any
		if err := json.Unmarshal([]byte(captured), &sent); err != nil {
			t.Fatalf("parse captured: %v", err)
		}
		got, _ := sent["model"].(string)
		if got != c.expected {
			t.Errorf("Chat(%q): upstream got model=%q, want %q", c.input, got, c.expected)
		}
	}
}

func TestFinishReasonFromOllama_PrefersDoneReason(t *testing.T) {
	// Phase 4 scrum fix B-4: done_reason takes precedence when present.
	cases := []struct {
		done       bool
		doneReason string
		want       string
	}{
		{true, "stop", "stop"},
		{true, "length", "length"}, // truncation surfaced — pre-fix lost this
		{true, "", "stop"},          // legacy fallback
		{false, "", "length"},
		{false, "stop", "stop"}, // done_reason wins regardless of done
	}
	for _, c := range cases {
		got := finishReasonFromOllama(c.done, c.doneReason)
		if got != c.want {
			t.Errorf("finishReasonFromOllama(%v, %q) = %q, want %q", c.done, c.doneReason, got, c.want)
		}
	}
}

func TestRegistry_AvailableLooksUpDirectly(t *testing.T) {
	// Phase 4 scrum fix B-2: Available() reports registry membership,
	// not routing. Unregistered name → false; registered + Available()
	// → true; registered but empty key → false.
	registered := newFake("openrouter", true)
	disabled := newFake("opencode", false) // "registered" but no key
	r := NewRegistry(registered, disabled)
	if !r.Available("openrouter") {
		t.Errorf("openrouter should be Available")
	}
	if r.Available("opencode") {
		t.Errorf("opencode has no key — Available should be false")
	}
	if r.Available("nonexistent") {
		t.Errorf("nonexistent should be false")
	}
}

func TestNewProviderConstructors(t *testing.T) {
	// Smoke: each public constructor produces a working provider with
	// the right name/prefix and respects empty-key=unavailable.
	or := NewOpenRouter("test-key", 0)
	if or.Name() != "openrouter" || !or.Available() {
		t.Errorf("openrouter constructor wrong: %s avail=%v", or.Name(), or.Available())
	}
	oc := NewOpenCode("test-key", 0)
	if oc.Name() != "opencode" || !oc.Available() {
		t.Errorf("opencode constructor wrong: %s avail=%v", oc.Name(), oc.Available())
	}
	km := NewKimi("", 0) // empty key → unavailable
	if km.Available() {
		t.Errorf("kimi with empty key should be unavailable")
	}
}
