package matrix

import "testing"

func TestIsWeakModel(t *testing.T) {
	cases := []struct {
		model string
		weak  bool
	}{
		// :free suffix → weak
		{"openai/gpt-4o:free", true},
		{"meta-llama/llama-3-8b:free", true},
		// :free/ infix (routing-prefixed names)
		{"openrouter:free/anthropic/claude-3.5-sonnet", true},
		// Local last-resort rungs
		{"qwen3.5:latest", true},
		{"qwen3:latest", true},
		// Strong by default
		{"x-ai/grok-4.1-fast", false},
		{"opencode/claude-opus-4-7", false},
		{"openai/gpt-5", false},
		{"qwen3-coder:480b", false}, // not the :latest tag
		{"", false},
	}
	for _, c := range cases {
		got := IsWeakModel(c.model)
		if got != c.weak {
			t.Errorf("IsWeakModel(%q): want %v, got %v", c.model, c.weak, got)
		}
	}
}

func TestMaybeDowngrade_TruthTable(t *testing.T) {
	cases := []struct {
		name string
		in   DowngradeInput
		want DowngradeDecision
	}{
		{
			name: "downgrade fires: lakehouse mode + strong model + no force",
			in:   DowngradeInput{Mode: ModeCodeReviewLakehouse, Model: "x-ai/grok-4.1-fast"},
			want: DowngradeDecision{
				Mode:           ModeCodeReviewIsolation,
				DowngradedFrom: ModeCodeReviewLakehouse,
			},
		},
		{
			name: "no downgrade: forced mode bypasses gate",
			in:   DowngradeInput{Mode: ModeCodeReviewLakehouse, Model: "x-ai/grok-4.1-fast", ForcedMode: true},
			want: DowngradeDecision{Mode: ModeCodeReviewLakehouse},
		},
		{
			name: "no downgrade: env override bypasses gate",
			in:   DowngradeInput{Mode: ModeCodeReviewLakehouse, Model: "x-ai/grok-4.1-fast", ForceFullOverride: true},
			want: DowngradeDecision{Mode: ModeCodeReviewLakehouse},
		},
		{
			name: "no downgrade: weak model keeps lakehouse",
			in:   DowngradeInput{Mode: ModeCodeReviewLakehouse, Model: "openai/gpt-4o:free"},
			want: DowngradeDecision{Mode: ModeCodeReviewLakehouse},
		},
		{
			name: "no downgrade: non-lakehouse mode (gate not applicable)",
			in:   DowngradeInput{Mode: "codereview_isolation", Model: "x-ai/grok-4.1-fast"},
			want: DowngradeDecision{Mode: "codereview_isolation"},
		},
	}
	for _, c := range cases {
		got := MaybeDowngrade(c.in)
		if got.Mode != c.want.Mode {
			t.Errorf("%s: Mode want %q, got %q", c.name, c.want.Mode, got.Mode)
		}
		if got.DowngradedFrom != c.want.DowngradedFrom {
			t.Errorf("%s: DowngradedFrom want %q, got %q", c.name, c.want.DowngradedFrom, got.DowngradedFrom)
		}
		if got.Reason == "" {
			t.Errorf("%s: Reason should be non-empty", c.name)
		}
	}
}

// TestIsWeakModelInList covers the config-driven variant added in
// Phase 2 — callers pass cfg.Models.WeakModels instead of relying on
// the package-level DefaultWeakModels.
func TestIsWeakModelInList(t *testing.T) {
	custom := []string{"my-custom-7b:latest", "qwen3:latest"}
	cases := []struct {
		model string
		list  []string
		weak  bool
	}{
		// Custom literal hits (config-driven)
		{"my-custom-7b:latest", custom, true},
		{"qwen3:latest", custom, true},
		// qwen3.5:latest is in DefaultWeakModels but NOT in custom list →
		// not weak when caller supplies custom list (intentional — operator
		// owns the list).
		{"qwen3.5:latest", custom, false},
		// :free/free-tier rules apply regardless of list
		{"openai/gpt-4o:free", custom, true},
		{"openrouter:free/anthropic/claude", custom, true},
		// Empty list → only rule-based checks apply
		{"qwen3.5:latest", []string{}, false},
		{"openai/gpt-4o:free", []string{}, true},
		// nil list behaves like empty (caller hasn't migrated yet)
		{"qwen3.5:latest", nil, false},
	}
	for _, c := range cases {
		got := IsWeakModelInList(c.model, c.list)
		if got != c.weak {
			t.Errorf("IsWeakModelInList(%q, %v): want %v, got %v", c.model, c.list, c.weak, got)
		}
	}
}

// TestMaybeDowngrade_WithConfigList verifies the DowngradeInput.WeakModels
// field overrides DefaultWeakModels when populated, so callers reading
// cfg.Models.WeakModels at startup get the configured behavior end-to-end.
func TestMaybeDowngrade_WithConfigList(t *testing.T) {
	// Config says "only my-custom is weak". qwen3.5:latest is now strong
	// (operator opted out of the local-hot-path treatment).
	configList := []string{"my-custom:latest"}
	in := DowngradeInput{
		Mode:       ModeCodeReviewLakehouse,
		Model:      "qwen3.5:latest",
		WeakModels: configList,
	}
	got := MaybeDowngrade(in)
	if got.Mode != ModeCodeReviewIsolation {
		t.Errorf("qwen3.5:latest with config list excluding it should downgrade; got Mode=%q", got.Mode)
	}
	if got.DowngradedFrom != ModeCodeReviewLakehouse {
		t.Errorf("DowngradedFrom should be lakehouse; got %q", got.DowngradedFrom)
	}

	// Same input but weak by config — gate must NOT fire.
	in.Model = "my-custom:latest"
	got = MaybeDowngrade(in)
	if got.Mode != ModeCodeReviewLakehouse {
		t.Errorf("my-custom:latest is in config list — should stay lakehouse; got %q", got.Mode)
	}
}

// TestMaybeDowngrade_ForcedTrumpsOthers verifies precedence: when
// multiple bypass conditions hit, ForcedMode wins (explicit caller
// intent always overrides). Caught a subtle ordering bug in the
// original Rust code where this was tested only by happy path.
func TestMaybeDowngrade_ForcedTrumpsOthers(t *testing.T) {
	in := DowngradeInput{
		Mode:              ModeCodeReviewLakehouse,
		Model:             "qwen3.5:latest", // weak — would otherwise hit weak-bypass
		ForcedMode:        true,
		ForceFullOverride: true,
	}
	got := MaybeDowngrade(in)
	if got.Mode != ModeCodeReviewLakehouse {
		t.Errorf("forced mode should keep mode: got %q", got.Mode)
	}
	if got.DowngradedFrom != "" {
		t.Errorf("no downgrade expected; got DowngradedFrom=%q", got.DowngradedFrom)
	}
}
