package modes

import (
	"bufio"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func load(t *testing.T) *Inventory {
	t.Helper()
	inv, err := Load()
	if err != nil {
		t.Fatal(err)
	}
	return inv
}

// The mapping back to the seven public labels must be total, ids unique, and a
// reject policy only where the region is open.
func TestInventoryShape(t *testing.T) {
	inv := load(t)
	if inv.EncoderID == "" {
		t.Error("no encoder_id: a fitted distance means nothing without the body it was fitted under")
	}
	valid := map[string]bool{}
	for _, i := range Intents {
		valid[i] = true
	}
	seen := map[string]bool{}
	for _, m := range inv.Modes {
		if seen[m.ID] {
			t.Errorf("%s: duplicate id", m.ID)
		}
		seen[m.ID] = true
		if !valid[m.Intent] {
			t.Errorf("%s: intent %q is not one of the seven", m.ID, m.Intent)
		}
		if m.Handler == "" {
			t.Errorf("%s: no handler, so it is not a mode", m.ID)
		}
		if m.SeparatedBy == "" {
			t.Errorf("%s: no separated_by, so nothing states the review obligation", m.ID)
		}
		if m.Open && m.RejectPolicy == "" {
			t.Errorf("%s: open with no reject_policy", m.ID)
		}
		if !m.Open && m.RejectPolicy != "" {
			t.Errorf("%s: reject_policy on a bounded mode", m.ID)
		}
		if m.PrototypeCount < 1 {
			t.Errorf("%s: prototype_count %d", m.ID, m.PrototypeCount)
		}
	}
	// No id is a prefix of another. act.tool.hoststats was, and it turned out to
	// run the same handler as act.tool: a read against a change is the tool row's
	// destructive field, which the confirm gate already reads. Handler is prose,
	// so a duplicated behaviour hides there. A nested id is the tell that shows.
	for _, a := range inv.Modes {
		for _, b := range inv.Modes {
			if a.ID != b.ID && strings.HasPrefix(b.ID, a.ID+".") {
				t.Errorf("%s is nested under %s, so one of them is not a mode", b.ID, a.ID)
			}
		}
	}
	// Nearest names a real mode, or the review obligation points at nothing.
	for _, m := range inv.Modes {
		if m.Nearest != "" && !seen[m.Nearest] {
			t.Errorf("%s: nearest %q is not in the inventory", m.ID, m.Nearest)
		}
		if m.Nearest == m.ID {
			t.Errorf("%s: nearest is itself", m.ID)
		}
	}
}

func repoRoot(t *testing.T) string {
	t.Helper()
	wd, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	return filepath.Join(wd, "..", "..")
}

func seedRows(t *testing.T) map[string]bool {
	t.Helper()
	paths, err := filepath.Glob(filepath.Join(repoRoot(t), "models", "seeds", "*.txt"))
	if err != nil || len(paths) == 0 {
		t.Fatalf("no seed files: %v", err)
	}
	out := map[string]bool{}
	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			t.Fatal(err)
		}
		sc := bufio.NewScanner(f)
		for sc.Scan() {
			line := strings.TrimSpace(sc.Text())
			if line == "" || strings.HasPrefix(line, "#") {
				continue
			}
			out[strings.ToLower(line)] = true
		}
		f.Close()
	}
	return out
}

// Every example is a real seed row. Not generated: 202 reviewed generated
// contrast pairs cost four points of fixture accuracy on 06-08-2026, and the
// generated half of the corpus recovers its own generation prompt when clustered.
func TestExamplesComeFromSeedRows(t *testing.T) {
	inv := load(t)
	seeds := seedRows(t)
	for _, m := range inv.Modes {
		if len(m.Examples) == 0 {
			continue
		}
		for _, e := range m.Examples {
			if !seeds[strings.ToLower(e)] {
				t.Errorf("%s: example %q is not a seed row", m.ID, e)
			}
		}
	}
}

// The fixture is the sole held-out measurement. An example drawn from it makes
// every number after that unfalsifiable.
func TestExamplesAreNotFixtureCases(t *testing.T) {
	inv := load(t)
	b, err := os.ReadFile(filepath.Join(repoRoot(t), "internal", "router", "eval", "ru_routing_v1.json"))
	if err != nil {
		t.Skipf("fixture not readable: %v", err)
	}
	var raw struct {
		Cases []struct {
			Utterance string `json:"utterance"`
		} `json:"cases"`
	}
	if err := json.Unmarshal(b, &raw); err != nil {
		t.Fatalf("fixture shape changed, and this invariant must not silently skip: %v", err)
	}
	held := map[string]bool{}
	for _, c := range raw.Cases {
		if c.Utterance != "" {
			held[strings.ToLower(strings.TrimSpace(c.Utterance))] = true
		}
	}
	if len(held) == 0 {
		t.Fatal("read no utterances from the fixture")
	}
	for _, m := range inv.Modes {
		for _, e := range m.Examples {
			if held[strings.ToLower(e)] {
				t.Errorf("%s: example %q is a fixture case", m.ID, e)
			}
		}
	}
}

// Not a failure, a report. Nine modes have zero real examples and they are the
// nine with no deterministic matcher, which is why V-629 and V-630 come before
// V-632: without persisted turns there is nothing to fit them from.
func TestFittableReport(t *testing.T) {
	inv := load(t)
	var ready, short, empty []string
	for _, m := range inv.Modes {
		switch {
		case len(m.Examples) == 0:
			empty = append(empty, m.ID)
		case m.Fittable():
			ready = append(ready, m.ID)
		default:
			short = append(short, m.ID)
		}
	}
	t.Logf("modes: %d total, %d ready to fit, %d short of min_seed_examples, %d with no seed example at all",
		len(inv.Modes), len(ready), len(short), len(empty))
	t.Logf("  no examples: %s", strings.Join(empty, ", "))
	t.Logf("  short: %s", strings.Join(short, ", "))
}
