package phraser

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

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

// fakeRemote — a workstation model that is up or down on command, and records
// what it was asked.
type fakeRemote struct {
	up    bool
	reply string
	err   error
	got   []llm.Req
}

func (f *fakeRemote) Available() bool { return f.up }

func (f *fakeRemote) CompleteRemote(_ context.Context, r llm.Req) (string, error) {
	f.got = append(f.got, r)
	if f.err != nil {
		return "", f.err
	}
	return f.reply, nil
}

// The three outcomes of the naming half, in one place. The middle one is the
// whole task: a gap he is told about, not an answer from the smaller model.
func TestPhraseWorldNamesTheGapOnlyWhenThereIsOne(t *testing.T) {
	answer := `{"response": "Небо голубое из-за рэлеевского рассеяния.", "mood": "neutral"}`

	t.Run("no workstation configured: the resident model answers as today", func(t *testing.T) {
		spy := newPromptSpy(t)
		p := NewLLMPhraserAt(spy.srv.URL, Config{})
		got, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil)
		if err != nil {
			t.Fatalf("PhraseWorld: %v", err)
		}
		if got == "" {
			t.Fatal("no reply from the resident model")
		}
		if len(spy.user) != 1 {
			t.Fatalf("resident model saw %d requests, want 1", len(spy.user))
		}
	})

	t.Run("workstation up: it answers and the resident model is not asked", func(t *testing.T) {
		spy := newPromptSpy(t)
		p := NewLLMPhraserAt(spy.srv.URL, Config{})
		remote := &fakeRemote{up: true, reply: answer}
		p.UseRemote(remote)
		got, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil)
		if err != nil {
			t.Fatalf("PhraseWorld: %v", err)
		}
		if !strings.Contains(got, "рассеяния") {
			t.Errorf("reply is not the workstation's: %q", got)
		}
		if len(spy.user) != 0 {
			t.Errorf("the resident model was asked %d times, want 0", len(spy.user))
		}
	})

	t.Run("workstation down: the gap, and nothing invented", func(t *testing.T) {
		spy := newPromptSpy(t)
		p := NewLLMPhraserAt(spy.srv.URL, Config{})
		p.UseRemote(&fakeRemote{up: false})
		got, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil)
		if !errors.Is(err, ErrNoWorldModel) {
			t.Fatalf("err = %v, want ErrNoWorldModel", err)
		}
		if got != "" {
			t.Errorf("got a reply %q with no world model", got)
		}
		if len(spy.user) != 0 {
			t.Errorf("the resident model answered a world question %d times, want 0", len(spy.user))
		}
	})

	t.Run("workstation errors mid-request: still the gap", func(t *testing.T) {
		spy := newPromptSpy(t)
		p := NewLLMPhraserAt(spy.srv.URL, Config{})
		p.UseRemote(&fakeRemote{up: true, err: errors.New("connection refused")})
		if _, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil); !errors.Is(err, ErrNoWorldModel) {
			t.Fatalf("err = %v, want ErrNoWorldModel", err)
		}
		if len(spy.user) != 0 {
			t.Errorf("the resident model answered a world question %d times, want 0", len(spy.user))
		}
	})
}

// Prompt parity: the workstation model is asked the same question in the same
// words, or the fixtures measure one thing and the daemon ships another.
func TestPhraseWorldSendsTheSamePromptsAsPhraseQuery(t *testing.T) {
	spy := newPromptSpy(t)
	resident := NewLLMPhraserAt(spy.srv.URL, Config{})
	if _, err := resident.PhraseQuery(context.Background(), "кто написал войну и мир", []string{"Лев Толстой"}); err != nil {
		t.Fatal(err)
	}

	remote := &fakeRemote{up: true, reply: `{"response": "Толстой.", "mood": "neutral"}`}
	offloaded := NewLLMPhraserAt(spy.srv.URL, Config{})
	offloaded.UseRemote(remote)
	if _, err := offloaded.PhraseWorld(context.Background(), "кто написал войну и мир", []string{"Лев Толстой"}); err != nil {
		t.Fatal(err)
	}

	if len(remote.got) != 1 {
		t.Fatalf("the workstation saw %d requests, want 1", len(remote.got))
	}
	if remote.got[0].System != spy.system[0] {
		t.Errorf("system prompts differ:\nremote:   %q\nresident: %q", remote.got[0].System, spy.system[0])
	}
	if remote.got[0].User != spy.user[0] {
		t.Errorf("user prompts differ:\nremote:   %q\nresident: %q", remote.got[0].User, spy.user[0])
	}
}

// The silent half. A nudge phrased on the workstation is not news, and one
// phrased here because the card is busy is not news either — but it must be
// sampled the same way, or the workstation quietly changes how she sounds.
func TestNudgePhrasingPrefersTheWorkstationSilently(t *testing.T) {
	spy := newPromptSpy(t)
	p := NewLLMPhraserAt(spy.srv.URL, Config{LLMNudges: true})
	remote := &fakeRemote{up: true, reply: `{"response": "Выпей воды.", "mood": "neutral"}`}
	p.UseRemote(remote)

	pn, err := p.PhraseNudge(context.Background(), loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1})
	if err != nil {
		t.Fatalf("PhraseNudge: %v", err)
	}
	if pn.Body != "Выпей воды." {
		t.Errorf("body = %q, want the workstation's wording", pn.Body)
	}
	if len(remote.got) != 1 {
		t.Fatalf("the workstation saw %d requests, want 1", len(remote.got))
	}
	if remote.got[0].Temperature != defaultChatTemperature {
		t.Errorf("temperature = %v, want %v (what the resident transport samples at)",
			remote.got[0].Temperature, defaultChatTemperature)
	}
	if len(spy.user) != 0 {
		t.Errorf("the resident model phrased %d nudges, want 0", len(spy.user))
	}
}

func TestNudgePhrasingFallsBackWhenTheCardIsBusy(t *testing.T) {
	spy := newPromptSpy(t)
	p := NewLLMPhraserAt(spy.srv.URL, Config{LLMNudges: true})
	p.UseRemote(&fakeRemote{up: false})

	if _, err := p.PhraseNudge(context.Background(), loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1}); err != nil {
		t.Fatalf("PhraseNudge: %v", err)
	}
	if len(spy.user) != 1 {
		t.Fatalf("the resident model phrased %d nudges, want 1", len(spy.user))
	}
}
