package email

import (
	"context"
	"strings"
	"testing"

	"github.com/kami/maven/internal/llm"
)

// fakeLLM returns a canned reply and records the request, so a test can assert
// on the grammar and on what of the mail was sent.
type fakeLLM struct {
	reply string
	err   error
	got   llm.Req
	calls int
}

func (f *fakeLLM) Complete(_ context.Context, r llm.Req) (string, error) {
	f.calls++
	f.got = r
	return f.reply, f.err
}

func msgFor(subject, body string) Message {
	return Message{UID: 1, From: "anton@example.org", Subject: subject, Body: body}
}

func TestExtractCandidates(t *testing.T) {
	f := &fakeLLM{reply: `[{"text":"отправить акт","due":""},{"text":"оплатить счёт","due":"2026-08-05"}]`}
	e := NewExtractor(f, 0, nil)
	got, err := e.Extract(context.Background(), msgFor("Акт и счёт", "Надо отправить акт и оплатить счёт до 5 августа."))
	if err != nil {
		t.Fatalf("extract: %v", err)
	}
	if len(got) != 2 {
		t.Fatalf("got %d candidates, want 2: %+v", len(got), got)
	}
	if got[0].Text != "отправить акт" || got[1].Due != "2026-08-05" {
		t.Errorf("candidates = %+v", got)
	}
	if f.got.Grammar == "" {
		t.Error("extraction must be grammar-constrained")
	}
	// The subject and body go to the model; nothing else about the message does.
	if !strings.Contains(f.got.User, "Акт и счёт") || !strings.Contains(f.got.User, "оплатить счёт") {
		t.Errorf("user turn = %q", f.got.User)
	}
}

func TestExtractEmptyArrayIsNotAnError(t *testing.T) {
	f := &fakeLLM{reply: "[]"}
	got, err := NewExtractor(f, 0, nil).Extract(context.Background(), msgFor("FYI", "Просто к сведению."))
	if err != nil || len(got) != 0 {
		t.Fatalf("got (%v, %v), want (empty, nil) — no task is the normal answer", got, err)
	}
}

// Junk must never reach the model: the header filter exists so the resident
// model is not spent on newsletters.
func TestExtractSkipsJunkWithoutCallingModel(t *testing.T) {
	f := &fakeLLM{reply: `[{"text":"купить всё со скидкой","due":""}]`}
	msg := msgFor("Скидки", "Sale!")
	msg.Junk = true
	got, err := NewExtractor(f, 0, nil).Extract(context.Background(), msg)
	if err != nil || got != nil {
		t.Fatalf("got (%v, %v), want (nil, nil)", got, err)
	}
	if f.calls != 0 {
		t.Errorf("model called %d times for junk, want 0", f.calls)
	}
}

func TestExtractEmptyMessageIsNotSent(t *testing.T) {
	f := &fakeLLM{reply: "[]"}
	if _, err := NewExtractor(f, 0, nil).Extract(context.Background(), Message{UID: 3}); err != nil {
		t.Fatalf("extract: %v", err)
	}
	if f.calls != 0 {
		t.Errorf("model called %d times for an empty message, want 0", f.calls)
	}
}

func TestExtractCaps(t *testing.T) {
	f := &fakeLLM{reply: `[{"text":"a","due":""},{"text":"b","due":""},{"text":"c","due":""}]`}
	got, err := NewExtractor(f, 2, nil).Extract(context.Background(), msgFor("s", "b"))
	if err != nil {
		t.Fatalf("extract: %v", err)
	}
	if len(got) != 2 {
		t.Errorf("got %d, want the configured cap of 2", len(got))
	}
}

func TestExtractDropsRepeatsAndBadDates(t *testing.T) {
	f := &fakeLLM{reply: `[{"text":"Отправить акт","due":"2026-02-31"},{"text":"отправить  акт","due":""},{"text":"  ","due":""}]`}
	got, err := NewExtractor(f, 0, nil).Extract(context.Background(), msgFor("s", "b"))
	if err != nil {
		t.Fatalf("extract: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("got %d candidates, want 1 (repeat and blank dropped): %+v", len(got), got)
	}
	if got[0].Due != "" {
		t.Errorf("due = %q, want empty — 2026-02-31 is not a date", got[0].Due)
	}
}

// A Thinking model sometimes wraps the array; and when it emits something
// unparsable the caller must hear about it rather than see "no tasks".
func TestParseCandidatesTolerance(t *testing.T) {
	got, err := parseCandidates("думаю... [{\"text\":\"x\",\"due\":\"\"}] всё")
	if err != nil || len(got) != 1 || got[0].Text != "x" {
		t.Fatalf("got (%+v, %v)", got, err)
	}
	if _, err := parseCandidates("нет никакого JSON"); err == nil {
		t.Error("unparsable output must be an error")
	}
}

func TestExtractParseErrorHidesMailText(t *testing.T) {
	f := &fakeLLM{reply: "он просил отправить акт, вот такой ответ"}
	_, err := NewExtractor(f, 0, nil).Extract(context.Background(), msgFor("Акт", "секретный текст"))
	if err == nil {
		t.Fatal("want an error")
	}
	if strings.Contains(err.Error(), "акт") || strings.Contains(err.Error(), "секретный") {
		t.Errorf("error text leaks mail content: %v", err)
	}
}

func TestParseDue(t *testing.T) {
	if _, ok := ParseDue(""); ok {
		t.Error("empty due must be (zero, false)")
	}
	if got, ok := ParseDue("2026-08-05"); !ok || got.Year() != 2026 || got.Month() != 8 || got.Day() != 5 {
		t.Errorf("ParseDue = (%v, %v)", got, ok)
	}
	if _, ok := ParseDue("05.08.2026"); ok {
		t.Error("a non-ISO date must not parse")
	}
}
