package llm

import (
	"bytes"
	"encoding/json"
	"testing"
)

// capability_test.go asserts the exact wire body the D3.1 resolver renders for
// each real provider quirk (00-provider-quirks): the hardcoded Phase-0 body
// 400s on Kimi / gpt-5 / Gemini, so the body shape IS the contract.

func marshalBody(t *testing.T, r openAIRequest) map[string]any {
	t.Helper()
	data, err := json.Marshal(r)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var m map[string]any
	if err := json.Unmarshal(data, &m); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	return m
}

func mustNum(t *testing.T, m map[string]any, k string, want float64) {
	t.Helper()
	v, ok := m[k]
	if !ok {
		t.Fatalf("key %q absent, want %v", k, want)
	}
	if f, ok := v.(float64); !ok || f != want {
		t.Fatalf("key %q = %v, want %v", k, v, want)
	}
}

func mustStr(t *testing.T, m map[string]any, k, want string) {
	t.Helper()
	if v, _ := m[k].(string); v != want {
		t.Fatalf("key %q = %v, want %q", k, m[k], want)
	}
}

func mustAbsent(t *testing.T, m map[string]any, k string) {
	t.Helper()
	if _, ok := m[k]; ok {
		t.Fatalf("key %q must be absent, got %v", k, m[k])
	}
}

func TestCapabilityWireBody(t *testing.T) {
	base := openAIRequest{
		model:       "m",
		messages:    []openAIMessage{{Role: "user", Content: "u"}},
		maxTokens:   4096,
		temperature: 0.4,
	}

	tests := []struct {
		name   string
		cap    Capability
		effort string
		extra  map[string]any
		check  func(t *testing.T, m map[string]any)
	}{
		{
			name:   "zero cap reproduces the Phase-0 wire",
			effort: "off",
			check: func(t *testing.T, m map[string]any) {
				mustNum(t, m, "max_tokens", 4096)
				mustNum(t, m, "temperature", 0.4)
				mustAbsent(t, m, "max_completion_tokens")
				mustAbsent(t, m, "reasoning_effort")
			},
		},
		{
			name:   "gpt-5: max_completion_tokens, omit temperature, off->minimal",
			cap:    Capability{Budget: BudgetMaxCompletionTokens, Temp: TempOmit, Reasoning: ReasoningCap{Control: ReasoningEffortField, OffEffort: "minimal"}},
			effort: "off",
			check: func(t *testing.T, m map[string]any) {
				mustNum(t, m, "max_completion_tokens", 4096)
				mustAbsent(t, m, "max_tokens")
				mustAbsent(t, m, "temperature")
				mustStr(t, m, "reasoning_effort", "minimal")
			},
		},
		{
			// The grok-4.3 editor money-path (consolidation tasks 1-2): reasoning=off must send an
			// EXPLICIT reasoning_effort:"none" — grok's OMITTED default is "low" (it thinks, billed
			// additively). A test that only checked the param is ABSENT would pass on the buggy
			// control:none wiring; this pins the param is PRESENT and equals "none" (eval's flagged hole).
			name:   "grok editor: control=effort off_effort=none sends explicit reasoning_effort:none",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningEffortField, OffEffort: "none"}},
			effort: "off",
			check: func(t *testing.T, m map[string]any) {
				mustStr(t, m, "reasoning_effort", "none")
			},
		},
		{
			name:   "gemini mandatory: swallow off, never disable, temperature sent",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningMandatory}},
			effort: "off",
			check: func(t *testing.T, m map[string]any) {
				mustNum(t, m, "max_tokens", 4096)
				mustNum(t, m, "temperature", 0.4)
				mustAbsent(t, m, "reasoning_effort")
				mustAbsent(t, m, "thinking")
			},
		},
		{
			name:   "gemini mandatory swallows a NON-off effort too (escalation at high still 400-safe)",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningMandatory}},
			effort: "high",
			check: func(t *testing.T, m map[string]any) {
				mustAbsent(t, m, "reasoning_effort")
				mustAbsent(t, m, "thinking")
				mustNum(t, m, "temperature", 0.4)
			},
		},
		{
			name:   "GLM extra_body_disable: off merges thinking:disabled, no reasoning_effort",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningExtraBodyDisable, OffExtraBody: map[string]any{"thinking": map[string]any{"type": "disabled"}}}},
			effort: "off",
			check: func(t *testing.T, m map[string]any) {
				mustAbsent(t, m, "reasoning_effort")
				th, ok := m["thinking"].(map[string]any)
				if !ok || th["type"] != "disabled" {
					t.Fatalf("thinking disable not merged: %v", m["thinking"])
				}
			},
		},
		{
			name:   "GLM extra_body_disable: UNSET effort also disables (Phase-0 unconditional-disable preserved)",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningExtraBodyDisable, OffExtraBody: map[string]any{"thinking": map[string]any{"type": "disabled"}}}},
			effort: "",
			check: func(t *testing.T, m map[string]any) {
				mustAbsent(t, m, "reasoning_effort")
				th, ok := m["thinking"].(map[string]any)
				if !ok || th["type"] != "disabled" {
					t.Fatalf("unset effort must still disable thinking (else GLM ×3 timeouts): %v", m["thinking"])
				}
			},
		},
		{
			name:   "GLM think-ON: on merges on_extra_body (D6.2)",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningExtraBodyDisable, OnExtraBody: map[string]any{"thinking": map[string]any{"type": "enabled"}}}},
			effort: "high",
			check: func(t *testing.T, m map[string]any) {
				th, ok := m["thinking"].(map[string]any)
				if !ok || th["type"] != "enabled" {
					t.Fatalf("think-ON not merged: %v", m["thinking"])
				}
			},
		},
		{
			name:   "kimi force temperature=1 regardless of the request",
			cap:    Capability{Temp: TempForce, TempValue: 1},
			effort: "off",
			check: func(t *testing.T, m map[string]any) {
				mustNum(t, m, "temperature", 1)
			},
		},
		{
			name:   "reasoning none passes low/medium/high through",
			cap:    Capability{Reasoning: ReasoningCap{Control: ReasoningNone}},
			effort: "medium",
			check: func(t *testing.T, m map[string]any) {
				mustStr(t, m, "reasoning_effort", "medium")
			},
		},
		{
			name:   "extra fills gaps only, never overrides a resolved key",
			effort: "off",
			extra:  map[string]any{"max_tokens": 999, "custom": "x"},
			check: func(t *testing.T, m map[string]any) {
				mustNum(t, m, "max_tokens", 4096) // resolved wins
				mustStr(t, m, "custom", "x")      // gap filled
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			r := base
			r.cap = tc.cap
			r.reasoningEffort = tc.effort
			r.extra = tc.extra
			m := marshalBody(t, r)
			if m["stream"] != false {
				t.Fatalf("stream must serialize as false, got %v", m["stream"])
			}
			tc.check(t, m)
		})
	}
}

// TestGLMMigrationWireIdentical proves the D3.1 migration of GLM's thinking
// disable from model.extra_body to capabilities.reasoning.off_extra_body leaves
// the WIRE body byte-identical — the snapshot changes (an intended one-time
// resnapshot) but not the request, so no live GLM call diverges (D5.2).
func TestGLMMigrationWireIdentical(t *testing.T) {
	msgs := []openAIMessage{{Role: "system", Content: "s"}, {Role: "user", Content: "u"}}
	legacy := openAIRequest{ // Phase-0 glm-5: default cap + extra_body disable switch
		model: "glm-5", messages: msgs, maxTokens: 4096, temperature: 0.4, reasoningEffort: "off",
		extra: map[string]any{"thinking": map[string]any{"type": "disabled"}},
	}
	migrated := openAIRequest{ // Phase-1 glm-5: disable via capabilities.reasoning.off_extra_body
		model: "glm-5", messages: msgs, maxTokens: 4096, temperature: 0.4, reasoningEffort: "off",
		cap: Capability{Reasoning: ReasoningCap{Control: ReasoningExtraBodyDisable, OffExtraBody: map[string]any{"thinking": map[string]any{"type": "disabled"}}}},
	}
	lb, err := json.Marshal(legacy)
	if err != nil {
		t.Fatal(err)
	}
	mb, err := json.Marshal(migrated)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(lb, mb) {
		t.Fatalf("GLM migration changed the wire body:\n legacy=%s\n migrated=%s", lb, mb)
	}
}
