// Tests for the Anthropic-Messages-protocol built-in providers (US-006, node
// #187): anthropic, minimax, minimax-cn. They assert the registry metadata
// (base_url, protocol, key env var) and that the constructed anthropic-compat
// driver attaches the auth header dictated by the provider's AuthScheme.
//
// No real network calls are made: the auth header is exercised by invoking the
// driver's authHeader func against a dummy *http.Request and inspecting the
// resulting headers (the same package can reach the unexported driver fields).
package provider

import (
	"encoding/json"
	"net/http"
	"testing"

	"github.com/smallnest/pigo/internal/agentcore"
)

// TestAnthropicProtocolProviderRegistrySpecs asserts the registry metadata for
// each Anthropic-Messages-protocol built-in: default base URL, wire protocol,
// and the key env var (via envAPIKey + t.Setenv).
func TestAnthropicProtocolProviderRegistrySpecs(t *testing.T) {
	cases := []struct {
		name        string
		wantBaseURL string
		wantEnv     string
	}{
		{"anthropic", "https://api.anthropic.com/v1", "ANTHROPIC_API_KEY"},
		{"minimax", "https://api.minimax.io/anthropic", "MINIMAX_API_KEY"},
		{"minimax-cn", "https://api.minimaxi.com/anthropic", "MINIMAX_CN_API_KEY"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			spec, ok := LookupProviderSpec(tc.name)
			if !ok {
				t.Fatalf("provider %q not found in registry", tc.name)
			}
			if spec.Protocol != ProtocolAnthropic {
				t.Errorf("protocol = %q, want %q", spec.Protocol, ProtocolAnthropic)
			}
			if spec.DefaultBaseURL != tc.wantBaseURL {
				t.Errorf("base_url = %q, want %q", spec.DefaultBaseURL, tc.wantBaseURL)
			}
			// Key resolution: the provider's key comes from tc.wantEnv.
			t.Setenv(tc.wantEnv, "SEKRET-"+tc.name)
			if got := envAPIKey(tc.name); got != "SEKRET-"+tc.name {
				t.Errorf("envAPIKey(%q) = %q, want key resolved from %s", tc.name, got, tc.wantEnv)
			}
		})
	}
}

// TestAnthropicProtocolProviderAuthHeader verifies that the driver built for
// each provider (the way resolveNamedProvider builds it: name + resolved
// base_url + spec.AuthScheme) targets the provider's base URL and sets the auth
// header matching its AuthScheme. anthropic/minimax/minimax-cn are all
// x-api-key + anthropic-version per pi's convention.
func TestAnthropicProtocolProviderAuthHeader(t *testing.T) {
	for _, name := range []string{"anthropic", "minimax", "minimax-cn"} {
		t.Run(name, func(t *testing.T) {
			spec, ok := LookupProviderSpec(name)
			if !ok {
				t.Fatalf("provider %q not found in registry", name)
			}
			p := NewAnthropicProtocolProvider(spec.Name, spec.DefaultBaseURL, spec.AuthScheme, nil)
			d, ok := p.(*anthropicCompatDriver)
			if !ok {
				t.Fatalf("provider %q is not an *anthropicCompatDriver", name)
			}
			if d.baseURL != spec.DefaultBaseURL {
				t.Errorf("baseURL = %q, want %q", d.baseURL, spec.DefaultBaseURL)
			}
			req, err := http.NewRequest(http.MethodPost, d.baseURL+"/messages", nil)
			if err != nil {
				t.Fatalf("build request: %v", err)
			}
			d.authHeader(req, "SEKRET")
			assertAuthHeaderForScheme(t, req, spec.AuthScheme)
			if got := req.Header.Get("Authorization"); got != "" && spec.AuthScheme != AuthBearer {
				t.Errorf("unexpected Authorization header %q for x-api-key scheme", got)
			}
		})
	}
}

// TestAnthropicAuthSchemeSelection proves the auth-header mechanism itself:
// AuthBearer yields Authorization: Bearer, AuthXAPIKey yields x-api-key plus the
// anthropic-version header. This guarantees an anthropic-protocol provider can
// select either header shape from its registry AuthScheme.
func TestAnthropicAuthSchemeSelection(t *testing.T) {
	t.Run("bearer", func(t *testing.T) {
		p := NewAnthropicProtocolProvider("gw", "https://example.test/anthropic", AuthBearer, nil)
		req := newDummyReq(t)
		p.(*anthropicCompatDriver).authHeader(req, "SEKRET")
		if got := req.Header.Get("Authorization"); got != "Bearer SEKRET" {
			t.Errorf("Authorization = %q, want %q", got, "Bearer SEKRET")
		}
		if got := req.Header.Get("x-api-key"); got != "" {
			t.Errorf("x-api-key = %q, want empty for bearer scheme", got)
		}
	})
	t.Run("x-api-key", func(t *testing.T) {
		p := NewAnthropicProtocolProvider("anthropic", "", AuthXAPIKey, nil)
		req := newDummyReq(t)
		p.(*anthropicCompatDriver).authHeader(req, "SEKRET")
		if got := req.Header.Get("x-api-key"); got != "SEKRET" {
			t.Errorf("x-api-key = %q, want %q", got, "SEKRET")
		}
		if got := req.Header.Get("anthropic-version"); got != anthropicAPIVersion {
			t.Errorf("anthropic-version = %q, want %q", got, anthropicAPIVersion)
		}
	})
	// A non-crashing fallback for unwired schemes (e.g. Bedrock's AuthAWS): must
	// not panic and defaults to the x-api-key convention.
	t.Run("fallback", func(t *testing.T) {
		p := NewAnthropicProtocolProvider("bedrock", "", AuthAWS, nil)
		req := newDummyReq(t)
		p.(*anthropicCompatDriver).authHeader(req, "SEKRET")
		if got := req.Header.Get("x-api-key"); got != "SEKRET" {
			t.Errorf("fallback x-api-key = %q, want %q", got, "SEKRET")
		}
	})
}

// TestNewAnthropicProviderUnchanged guards against regressing the direct
// Anthropic constructor: it must still default to the public endpoint and use
// x-api-key + anthropic-version.
func TestNewAnthropicProviderUnchanged(t *testing.T) {
	d, ok := NewAnthropicProvider("", nil).(*anthropicCompatDriver)
	if !ok {
		t.Fatal("NewAnthropicProvider did not return *anthropicCompatDriver")
	}
	if d.baseURL != anthropicBaseURL {
		t.Errorf("baseURL = %q, want %q", d.baseURL, anthropicBaseURL)
	}
	req := newDummyReq(t)
	d.authHeader(req, "SEKRET")
	if got := req.Header.Get("x-api-key"); got != "SEKRET" {
		t.Errorf("x-api-key = %q, want %q", got, "SEKRET")
	}
	if got := req.Header.Get("anthropic-version"); got != anthropicAPIVersion {
		t.Errorf("anthropic-version = %q, want %q", got, anthropicAPIVersion)
	}
}

func newDummyReq(t *testing.T) *http.Request {
	t.Helper()
	req, err := http.NewRequest(http.MethodPost, "https://example.test/messages", nil)
	if err != nil {
		t.Fatalf("build request: %v", err)
	}
	return req
}

func assertAuthHeaderForScheme(t *testing.T, req *http.Request, scheme string) {
	t.Helper()
	if scheme == AuthBearer {
		if got := req.Header.Get("Authorization"); got != "Bearer SEKRET" {
			t.Errorf("Authorization = %q, want %q", got, "Bearer SEKRET")
		}
		return
	}
	if got := req.Header.Get("x-api-key"); got != "SEKRET" {
		t.Errorf("x-api-key = %q, want %q", got, "SEKRET")
	}
	if got := req.Header.Get("anthropic-version"); got != anthropicAPIVersion {
		t.Errorf("anthropic-version = %q, want %q", got, anthropicAPIVersion)
	}
}

// TestEncodeAnthropicRequestIncludesModel guards a required field of the
// Anthropic Messages API: the request body must carry the "model" id. Omitting
// it makes the public API return 400 and OpenAI-compatible gateways return an
// empty/error response, which the SSE decoder silently turns into an empty
// assistant turn — a confusing "no output, no error" failure.
func TestEncodeAnthropicRequestIncludesModel(t *testing.T) {
	req := CompletionRequest{
		Model: "claude-opus-4-8",
	}
	body, err := encodeAnthropicRequest(req, nil)
	if err != nil {
		t.Fatalf("encodeAnthropicRequest: %v", err)
	}
	var decoded map[string]any
	if err := json.Unmarshal(body, &decoded); err != nil {
		t.Fatalf("unmarshal body: %v", err)
	}
	if got, ok := decoded["model"]; !ok {
		t.Fatalf("request body missing required \"model\" field; keys=%v", keysOf(decoded))
	} else if got != "claude-opus-4-8" {
		t.Errorf("model = %v, want claude-opus-4-8", got)
	}
}

func keysOf(m map[string]any) []string {
	ks := make([]string, 0, len(m))
	for k := range m {
		ks = append(ks, k)
	}
	return ks
}

// TestEncodeAnthropicRequestCoalescesToolResults guards the Anthropic protocol
// rule that every tool_use must be answered by tool_result blocks in a single
// immediately-following user message. Before the fix, each tool result was
// encoded as its own user turn, which the API rejected with "tool_use ids were
// found without tool_result blocks immediately after" whenever one assistant
// turn carried multiple tool_use calls.
func TestEncodeAnthropicRequestCoalescesToolResults(t *testing.T) {
	req := CompletionRequest{
		Model: "claude-x",
		Context: LlmContext{
			Messages: agentcore.MessageList{
				// Assistant turn with two tool_use blocks.
				agentcore.AssistantMessage{
					Content: agentcore.ContentList{
						agentcore.NewToolCallContent("call_1", "read", json.RawMessage(`{}`)),
						agentcore.NewToolCallContent("call_2", "write", json.RawMessage(`{}`)),
					},
				},
				// Two consecutive tool results for that turn.
				agentcore.ToolResultMessage{
					ToolCallID: "call_1",
					Content:    agentcore.ContentList{agentcore.NewTextContent("result-1")},
				},
				agentcore.ToolResultMessage{
					ToolCallID: "call_2",
					Content:    agentcore.ContentList{agentcore.NewTextContent("result-2")},
				},
				// Next assistant turn (text reply).
				agentcore.AssistantMessage{
					Content: agentcore.ContentList{agentcore.NewTextContent("done")},
				},
			},
		},
	}

	body, err := encodeAnthropicRequest(req, nil)
	if err != nil {
		t.Fatalf("encodeAnthropicRequest: %v", err)
	}
	decoded := decodeBody(t, body)
	msgs, ok := decoded["messages"].([]any)
	if !ok {
		t.Fatalf("messages not an array: %T", decoded["messages"])
	}
	if len(msgs) != 3 {
		t.Fatalf("want 3 top-level messages (assistant tool_use, one user tool_result turn, assistant reply), got %d:\n%v", len(msgs), msgs)
	}

	// Message 2 must be the single user turn holding BOTH tool_result blocks.
	second, ok := msgs[1].(map[string]any)
	if !ok {
		t.Fatalf("message[1] not an object: %T", msgs[1])
	}
	if second["role"] != "user" {
		t.Fatalf("message[1].role = %v, want user", second["role"])
	}
	blocks, ok := second["content"].([]any)
	if !ok {
		t.Fatalf("message[1].content not an array: %T", second["content"])
	}
	if len(blocks) != 2 {
		t.Fatalf("want 2 tool_result blocks in the coalesced turn, got %d:\n%v", len(blocks), second["content"])
	}
	for i, wantID := range []string{"call_1", "call_2"} {
		block, ok := blocks[i].(map[string]any)
		if !ok {
			t.Fatalf("block[%d] not an object: %T", i, blocks[i])
		}
		if block["type"] != "tool_result" || block["tool_use_id"] != wantID {
			t.Errorf("block[%d] = %v, want tool_result for %s", i, block, wantID)
		}
	}
}
