package chat

import (
	"context"
	"errors"
	"strings"
	"testing"
)

// recordingCompleter captures the system+user it was asked with and returns a
// canned answer (or error). It also records which model alias built it.
type recordingCompleter struct {
	model      string
	lastSystem string
	lastUser   string
	answer     string
	err        error
	calls      *int
}

func (c *recordingCompleter) Complete(_ context.Context, system, user string) (string, error) {
	*c.calls++
	c.lastSystem = system
	c.lastUser = user
	if c.err != nil {
		return "", c.err
	}
	return c.answer, nil
}

// factory builds a recordingCompleter per model and records the last one built so
// the test can assert which model alias was actually used for the gateway call.
type factory struct {
	answer string
	err    error
	calls  int
	used   *recordingCompleter
}

func (f *factory) make(model string) Completer {
	c := &recordingCompleter{model: model, answer: f.answer, err: f.err, calls: &f.calls}
	f.used = c
	return c
}

func TestModelsAreOfferedLocalFirstAndCopied(t *testing.T) {
	f := &factory{answer: "ok"}
	s := New(f.make, []string{"koala/phi4-mini", "iguana/gemma4-26b"}, 0)

	got := s.Models()
	want := []string{"koala/phi4-mini", "iguana/gemma4-26b"}
	if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("Models() = %v, want %v", got, want)
	}
	// Mutating the returned slice must not corrupt the Service's list.
	got[0] = "tampered"
	if s.Models()[0] != "koala/phi4-mini" {
		t.Fatal("Models() leaked its backing slice")
	}
}

func TestDefaultModelIsTheSummarysModelWhenOffered(t *testing.T) {
	f := &factory{answer: "ok"}
	s := New(f.make, []string{"koala/phi4-mini", "iguana/gemma4-26b", "berget/mistral-small"}, 0)

	if got := s.DefaultModel("iguana/gemma4-26b"); got != "iguana/gemma4-26b" {
		t.Fatalf("DefaultModel(summary) = %q, want the summary's model", got)
	}
	// A summary model no longer offered (e.g. cloud disabled) falls back to first.
	if got := s.DefaultModel("berget/old-model"); got != "koala/phi4-mini" {
		t.Fatalf("DefaultModel(un-offered) = %q, want the first offered model", got)
	}
	// No summary model recorded → first offered.
	if got := s.DefaultModel(""); got != "koala/phi4-mini" {
		t.Fatalf("DefaultModel(\"\") = %q, want the first offered model", got)
	}
}

func TestAnswerGroundsOnTranscriptAndCarriesHistory(t *testing.T) {
	f := &factory{answer: "  The video is about attention.  "}
	s := New(f.make, []string{"koala/phi4-mini"}, 0)

	reply, err := s.Answer(context.Background(), Request{
		Model:      "koala/phi4-mini",
		Transcript: "ATTENTION-TRANSCRIPT-MARKER",
		History:    []Turn{{Question: "who", Answer: "the host"}},
		Question:   "what is it about",
	})
	if err != nil {
		t.Fatalf("Answer: %v", err)
	}
	if reply.Answer != "The video is about attention." {
		t.Fatalf("answer not trimmed: %q", reply.Answer)
	}
	if reply.Truncated {
		t.Fatal("short transcript must not report truncated")
	}
	// The transcript rides in the system prompt; the conversation in the user msg.
	if !strings.Contains(f.used.lastSystem, "ATTENTION-TRANSCRIPT-MARKER") {
		t.Fatal("transcript not grounded into the system prompt")
	}
	if !strings.Contains(f.used.lastUser, "Q: who") || !strings.Contains(f.used.lastUser, "A: the host") {
		t.Fatalf("history not carried into the user message: %q", f.used.lastUser)
	}
	if !strings.Contains(f.used.lastUser, "what is it about") {
		t.Fatal("new question missing from the user message")
	}
}

func TestAnswerTruncatesLongTranscriptAndReportsIt(t *testing.T) {
	f := &factory{answer: "answer"}
	s := New(f.make, []string{"koala/phi4-mini"}, 10)

	reply, err := s.Answer(context.Background(), Request{
		Model:      "koala/phi4-mini",
		Transcript: strings.Repeat("x", 500),
		Question:   "summarize",
	})
	if err != nil {
		t.Fatalf("Answer: %v", err)
	}
	if !reply.Truncated {
		t.Fatal("a transcript past maxChars must report Truncated")
	}
	if strings.Count(f.used.lastSystem, "x") != 10 {
		t.Fatalf("transcript not bounded to maxChars: got %d x's", strings.Count(f.used.lastSystem, "x"))
	}
}

func TestAnswerForcesAnUnofferedModelBackToDefault(t *testing.T) {
	f := &factory{answer: "answer"}
	s := New(f.make, []string{"koala/phi4-mini", "iguana/gemma4-26b"}, 0)

	// A forged/un-offered model must never reach the gateway as-is — it is forced
	// to the default offered model (the cloud-absent guarantee depends on this).
	_, err := s.Answer(context.Background(), Request{
		Model:      "berget/secret-cloud-model",
		Transcript: "t",
		Question:   "q",
	})
	if err != nil {
		t.Fatalf("Answer: %v", err)
	}
	if f.used.model != "koala/phi4-mini" {
		t.Fatalf("un-offered model reached the gateway as %q, want the default", f.used.model)
	}
}

func TestAnswerPropagatesCompleterError(t *testing.T) {
	f := &factory{err: errors.New("gateway down")}
	s := New(f.make, []string{"koala/phi4-mini"}, 0)

	_, err := s.Answer(context.Background(), Request{Model: "koala/phi4-mini", Transcript: "t", Question: "q"})
	if err == nil {
		t.Fatal("expected the gateway error to propagate")
	}
}

func TestAnswerRejectsEmptyModelSet(t *testing.T) {
	s := New(func(string) Completer { return nil }, nil, 0)
	if _, err := s.Answer(context.Background(), Request{Question: "q"}); err == nil {
		t.Fatal("expected an error when no models are configured")
	}
}
