package provider

// Tests for provider resolution moved from cmd/pigo (US-004, #361): ResolveProvider
// maps a model id to the right gateway (preset catalog first, then prefix rules,
// then OpenRouter default), and ResolveBaseURL applies the base-url override
// precedence. Environment lookups are injected via os.Getenv here.

import (
	"os"
	"strings"
	"testing"
)

// TestResolveProviderPresetCatalog verifies a preset id resolves to its declared
// provider (NVIDIA and Ollama presets do not fall through to OpenRouter).
func TestResolveProviderPresetCatalog(t *testing.T) {
	cases := []struct {
		model    string
		wantName string
	}{
		{"meta/llama-3.3-70b-instruct", "nvidia"},     // NVIDIA preset
		{"ollama/llama3.3", "ollama"},                 // Ollama preset
		{"openai/gpt-4o", "openrouter"},               // OpenRouter preset
		{"anthropic/claude-3.5-sonnet", "openrouter"}, // OpenRouter preset
	}
	for _, c := range cases {
		_, name, err := ResolveProvider(c.model, "", "", "", os.Getenv)
		if err != nil {
			t.Errorf("ResolveProvider(%q) error: %v", c.model, err)
			continue
		}
		if name != c.wantName {
			t.Errorf("ResolveProvider(%q) = %q, want %q", c.model, name, c.wantName)
		}
	}
}

// TestResolveProviderPrefixAndDefault verifies the prefix rules and the
// OpenRouter default for ids not in the catalog.
func TestResolveProviderPrefixAndDefault(t *testing.T) {
	cases := []struct {
		model    string
		baseURL  string
		wantName string
	}{
		{"ollama/some-local-model", "", "ollama"}, // ollama/ prefix
		{"nvidia/some-nim-model", "", "nvidia"},   // nvidia/ prefix
		{"some-unknown-model", "", "openrouter"},  // default
		{"m", "http://host:11434/v1", "ollama"},   // ollama port
	}
	for _, c := range cases {
		_, name, err := ResolveProvider(c.model, c.baseURL, "", "", os.Getenv)
		if err != nil {
			t.Errorf("ResolveProvider(%q) error: %v", c.model, err)
			continue
		}
		if name != c.wantName {
			t.Errorf("ResolveProvider(%q, %q) = %q, want %q", c.model, c.baseURL, name, c.wantName)
		}
	}
}

// TestResolveProviderExplicitProtocol verifies an explicit --protocol wins over
// model-id heuristics: openai (with base-url) and anthropic select the matching
// wire driver, an empty base-url for openai errors, and an unknown protocol
// errors instead of silently falling back.
func TestResolveProviderExplicitProtocol(t *testing.T) {
	// openai protocol → "openai" provider name, requires base-url.
	if _, name, err := ResolveProvider("any-model", "https://example.com/v1", "openai", "", os.Getenv); err != nil || name != "openai" {
		t.Errorf("protocol=openai = (%q, %v), want (openai, nil)", name, err)
	}
	if _, _, err := ResolveProvider("any-model", "", "openai", "", os.Getenv); err == nil {
		t.Error("protocol=openai with no base-url should error")
	}
	// anthropic protocol → "anthropic" provider name, base-url optional (defaults).
	if _, name, err := ResolveProvider("claude-x", "", "anthropic", "", os.Getenv); err != nil || name != "anthropic" {
		t.Errorf("protocol=anthropic = (%q, %v), want (anthropic, nil)", name, err)
	}
	// Unknown protocol errors rather than falling back to a heuristic.
	if _, _, err := ResolveProvider("any-model", "", "grpc", "", os.Getenv); err == nil {
		t.Error("unknown protocol should error")
	}
}

// TestResolveProviderResponsesProtocol verifies the openai/resp_api selector
// routes to the Responses driver (against an explicit base-url), that the
// "openai/chat" alias resolves identically to "openai", and that resp_api with
// no base-url errors like the plain openai path (mirroring the base-url
// requirement rather than defaulting to a public endpoint).
func TestResolveProviderResponsesProtocol(t *testing.T) {
	// openai/resp_api → "openai" provider name, backed by the Responses driver.
	p, name, err := ResolveProvider("any-model", "https://example.com/v1", "openai/resp_api", "", os.Getenv)
	if err != nil || name != "openai" {
		t.Fatalf("protocol=openai/resp_api = (%q, %v), want (openai, nil)", name, err)
	}
	if _, ok := p.(*responsesDriver); !ok {
		t.Errorf("protocol=openai/resp_api built %T, want *responsesDriver", p)
	}
	// resp_api with no base-url errors, mirroring the openai requirement.
	if _, _, err := ResolveProvider("any-model", "", "openai/resp_api", "", os.Getenv); err == nil {
		t.Error("protocol=openai/resp_api with no base-url should error")
	}
	// "openai/chat" is an alias of "openai": same driver, same base-url rule.
	p, name, err = ResolveProvider("any-model", "https://example.com/v1", "openai/chat", "", os.Getenv)
	if err != nil || name != "openai" {
		t.Fatalf("protocol=openai/chat = (%q, %v), want (openai, nil)", name, err)
	}
	if _, ok := p.(*responsesDriver); ok {
		t.Error("protocol=openai/chat should build the Chat Completions driver, not *responsesDriver")
	}
	if _, _, err := ResolveProvider("any-model", "", "openai/chat", "", os.Getenv); err == nil {
		t.Error("protocol=openai/chat with no base-url should error")
	}
}

// TestResolveProviderExplicitProvider verifies that --provider selects a
// built-in provider from the registry: the returned provider-name is the spec
// name (so key resolution reads the right env var), an OpenAI-protocol provider
// (deepseek) and an Anthropic-protocol provider (minimax) both resolve, an
// incompatible --protocol is a conflict error naming both flags, and an unknown
// provider name errors while listing the available names.
func TestResolveProviderExplicitProvider(t *testing.T) {
	// OpenAI-protocol provider: returns its own name for key lookup.
	if _, name, err := ResolveProvider("deepseek-chat", "", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
		t.Errorf("provider=deepseek = (%q, %v), want (deepseek, nil)", name, err)
	}
	// Anthropic-protocol provider.
	if _, name, err := ResolveProvider("MiniMax-M2", "", "", "minimax", os.Getenv); err != nil || name != "minimax" {
		t.Errorf("provider=minimax = (%q, %v), want (minimax, nil)", name, err)
	}
	// A matching --protocol is not a conflict (deepseek speaks openai/resp_api).
	if _, name, err := ResolveProvider("deepseek-chat", "", "openai/resp_api", "deepseek", os.Getenv); err != nil || name != "deepseek" {
		t.Errorf("provider=deepseek + protocol=openai/resp_api = (%q, %v), want (deepseek, nil)", name, err)
	}
	// --provider wins over model-id heuristics: an ollama/-prefixed id still
	// resolves to the named provider, not local Ollama.
	if _, name, err := ResolveProvider("ollama/x", "", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
		t.Errorf("provider=deepseek with ollama/ model = (%q, %v), want (deepseek, nil)", name, err)
	}
	// --base-url overrides the spec default without changing the provider name.
	if _, name, err := ResolveProvider("deepseek-chat", "https://proxy.local/v1", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
		t.Errorf("provider=deepseek + base-url = (%q, %v), want (deepseek, nil)", name, err)
	}
	// Conflict: minimax speaks anthropic; forcing --protocol openai errors and
	// names both flags.
	_, _, err := ResolveProvider("MiniMax-M2", "", "openai", "minimax", os.Getenv)
	if err == nil {
		t.Fatal("provider=minimax + protocol=openai should conflict")
	}
	if !strings.Contains(err.Error(), "--provider") || !strings.Contains(err.Error(), "--protocol") {
		t.Errorf("conflict error should name both flags, got: %v", err)
	}
	// Unknown provider errors and lists available names.
	_, _, err = ResolveProvider("m", "", "", "no-such-provider", os.Getenv)
	if err == nil {
		t.Fatal("unknown provider should error")
	}
	if !strings.Contains(err.Error(), "deepseek") {
		t.Errorf("unknown-provider error should list available names, got: %v", err)
	}
	// An invalid --protocol paired with a named provider surfaces the clear
	// "unknown --protocol" error (listing the accepted set) rather than a
	// misleading conflict message.
	_, _, err = ResolveProvider("deepseek-chat", "", "openai_api", "deepseek", os.Getenv)
	if err == nil {
		t.Fatal("provider=deepseek + protocol=openai_api should error")
	}
	if !strings.Contains(err.Error(), "unknown --protocol") {
		t.Errorf("invalid --protocol should surface the unknown-protocol error, got: %v", err)
	}
}

// TestResolveProviderCNPresets verifies the Chinese-cloud preset ids route to
// their own provider (not the OpenRouter default) via the LookupPreset branch.
func TestResolveProviderCNPresets(t *testing.T) {
	cases := []struct {
		model    string
		wantName string
	}{
		{"ernie-4.5-turbo-32k", "qianfan"},
		{"doubao-seed-1-6", "volcengine"},
		{"qwen-max", "dashscope"},
		{"hunyuan-turbos-latest", "hunyuan"},
	}
	for _, c := range cases {
		_, name, err := ResolveProvider(c.model, "", "", "", os.Getenv)
		if err != nil {
			t.Errorf("ResolveProvider(%q) error: %v", c.model, err)
			continue
		}
		if name != c.wantName {
			t.Errorf("ResolveProvider(%q) = %q, want %q", c.model, name, c.wantName)
		}
	}
}

// TestResolveProviderCNExplicit verifies --provider selects the CN providers
// directly and that --base-url overrides without changing the provider name.
func TestResolveProviderCNExplicit(t *testing.T) {
	for _, name := range []string{"qianfan", "volcengine", "dashscope", "hunyuan"} {
		if _, got, err := ResolveProvider("some-model", "", "", name, os.Getenv); err != nil || got != name {
			t.Errorf("provider=%s = (%q, %v), want (%s, nil)", name, got, err, name)
		}
		if _, got, err := ResolveProvider("some-model", "https://proxy.local/v1", "", name, os.Getenv); err != nil || got != name {
			t.Errorf("provider=%s + base-url = (%q, %v), want (%s, nil)", name, got, err, name)
		}
	}
}

// TestResolveProviderModelNameInference verifies model-name inference (Issue
// #235): with only --model given, a bare model name whose prefix identifies a
// single provider resolves to that provider — NOT the OpenRouter default.
func TestResolveProviderModelNameInference(t *testing.T) {
	cases := []struct {
		model    string
		wantName string
	}{
		{"claude-opus-4-8", "anthropic"},
		{"deepseek-chat", "deepseek"},
		{"gpt-4.1", "openai"},
		{"gemini-3-pro", "google"},
		{"grok-5", "xai"},
	}
	for _, c := range cases {
		if _, name, err := ResolveProvider(c.model, "", "", "", os.Getenv); err != nil || name != c.wantName {
			t.Errorf("ResolveProvider(%q) = (%q, %v), want (%q, nil)", c.model, name, err, c.wantName)
		}
	}
}

// TestResolveProviderInferencePrecedence verifies that model-name inference does
// not override explicit flags and does not fire when a --base-url is given, and
// that unknown/ambiguous names still fall back to OpenRouter.
func TestResolveProviderInferencePrecedence(t *testing.T) {
	// Explicit --provider wins over an inferable model name.
	if _, name, err := ResolveProvider("claude-opus-4-8", "", "", "deepseek", os.Getenv); err != nil || name != "deepseek" {
		t.Errorf("provider=deepseek overrides inference = (%q, %v), want (deepseek, nil)", name, err)
	}
	// Explicit --protocol wins over an inferable model name.
	if _, name, err := ResolveProvider("claude-opus-4-8", "https://example.com/v1", "openai", "", os.Getenv); err != nil || name != "openai" {
		t.Errorf("protocol=openai overrides inference = (%q, %v), want (openai, nil)", name, err)
	}
	// A --base-url signals a custom endpoint: inference is skipped, default applies.
	if _, name, err := ResolveProvider("claude-opus-4-8", "https://gw.local/v1", "", "", os.Getenv); err != nil || name != "openrouter" {
		t.Errorf("inference skipped with base-url = (%q, %v), want (openrouter, nil)", name, err)
	}
	// Ambiguous/unknown names still default to OpenRouter.
	for _, m := range []string{"llama-3.3-70b", "totally-unknown-model"} {
		if _, name, err := ResolveProvider(m, "", "", "", os.Getenv); err != nil || name != "openrouter" {
			t.Errorf("ResolveProvider(%q) = (%q, %v), want (openrouter, nil)", m, name, err)
		}
	}
}

// TestResolveBaseURLPrecedence exercises all four precedence levels for a
// hyphenated provider (zai-coding-cn → ZAI_CODING_CN_BASE_URL).
func TestResolveBaseURLPrecedence(t *testing.T) {
	spec, ok := LookupProviderSpec("zai-coding-cn")
	if !ok {
		t.Fatal("expected zai-coding-cn in registry")
	}
	if got := ResolveBaseURL(spec, "", os.Getenv); got != spec.DefaultBaseURL {
		t.Errorf("default: got %q, want %q", got, spec.DefaultBaseURL)
	}
	t.Setenv("ZAI_CODING_CN_BASE_URL", "https://generic.example/v4")
	if got := ResolveBaseURL(spec, "", os.Getenv); got != "https://generic.example/v4" {
		t.Errorf("generic env: got %q, want %q", got, "https://generic.example/v4")
	}
	if got := ResolveBaseURL(spec, "https://flag.example/v4", os.Getenv); got != "https://flag.example/v4" {
		t.Errorf("flag over generic: got %q, want %q", got, "https://flag.example/v4")
	}
}

// TestResolveBaseURLProviderSpecificEnv covers a provider that declares a
// provider-specific base-url env var (azure), asserting it sits between the flag
// and the generic convention in precedence.
func TestResolveBaseURLProviderSpecificEnv(t *testing.T) {
	spec, ok := LookupProviderSpec("azure-openai-responses")
	if !ok {
		t.Fatal("expected azure-openai-responses in registry")
	}
	if len(spec.BaseURLEnvVars) == 0 {
		t.Fatal("expected azure-openai-responses to declare BaseURLEnvVars")
	}
	t.Setenv("AZURE_OPENAI_BASE_URL", "https://specific.example")
	if got := ResolveBaseURL(spec, "", os.Getenv); got != "https://specific.example" {
		t.Errorf("provider-specific env: got %q, want %q", got, "https://specific.example")
	}
	t.Setenv("AZURE_OPENAI_RESPONSES_BASE_URL", "https://generic.example")
	if got := ResolveBaseURL(spec, "", os.Getenv); got != "https://specific.example" {
		t.Errorf("provider-specific beats generic: got %q, want %q", got, "https://specific.example")
	}
	if got := ResolveBaseURL(spec, "https://flag.example", os.Getenv); got != "https://flag.example" {
		t.Errorf("flag beats provider-specific: got %q, want %q", got, "https://flag.example")
	}
}
