package provider

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"GoNavi-Wails/internal/ai"
)

func TestBuildClaudeCLIEnv_IncludesAnthropicProxyEnv(t *testing.T) {
	env, err := buildClaudeCLIEnv(ai.ProviderConfig{
		BaseURL: "https://proxy.example.com/",
		APIKey:  "sk-test",
	}, []string{"PATH=/usr/bin"}, "darwin", func(name string) (string, error) {
		return "", errors.New("unexpected lookup")
	}, func(path string) bool {
		return false
	})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if got := envValue(env, "ANTHROPIC_BASE_URL"); got != "https://proxy.example.com" {
		t.Fatalf("expected trimmed base url, got %q", got)
	}
	if got := envValue(env, "ANTHROPIC_API_KEY"); got != "sk-test" {
		t.Fatalf("expected api key in env, got %q", got)
	}
	if got := envValue(env, "ANTHROPIC_AUTH_TOKEN"); got != "sk-test" {
		t.Fatalf("expected auth token in env, got %q", got)
	}
}

func TestBuildClaudeCLIRequestLogBodyRedactsSecretsAndKeepsRequestShape(t *testing.T) {
	prompt := "请分析订单表。临时凭证 Bearer abcdefghijklmnopqrstuvwxyz，另一个 key 是 sk-live-abcdefghijklmnopqrstuvwxyz"
	args := []string{"-p", prompt, "--output-format", "stream-json", "--model", "claude-sonnet"}
	originalPromptArg := args[1]

	body := buildClaudeCLIRequestLogBody("stream-json", args, prompt, ai.ProviderConfig{
		BaseURL: "https://proxy.example.com/api/anthropic?key=proxy-secret&alt=sse",
		APIKey:  "sk-config-secret-1234567890",
		Model:   "claude-sonnet",
	}, ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: prompt}},
		Tools: []ai.Tool{{
			Type: "function",
			Function: ai.ToolFunction{
				Name:        "inspect_ai_upstream_logs",
				Description: "读取 AI 上游请求日志",
			},
		}},
	})

	got := formatAIUpstreamRequestLogBody(body)

	for _, want := range []string{
		`"command":"claude"`,
		`"output_format":"stream-json"`,
		`"model":"claude-sonnet"`,
		`"has_api_key":true`,
		`"message_count":1`,
		`"tool_count":1`,
		`"[prompt logged separately]"`,
		`inspect_ai_upstream_logs`,
		`key=%5BREDACTED%5D`,
		`[REDACTED]`,
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("expected Claude CLI request log body to contain %q, got %s", want, got)
		}
	}

	for _, leaked := range []string{
		"proxy-secret",
		"sk-config-secret",
		"Bearer abcdefghijklmnopqrstuvwxyz",
		"sk-live-abcdefghijklmnopqrstuvwxyz",
	} {
		if strings.Contains(got, leaked) {
			t.Fatalf("Claude CLI request log body leaked %q: %s", leaked, got)
		}
	}
	if args[1] != originalPromptArg {
		t.Fatalf("expected original args to remain unchanged, got %q", args[1])
	}
}

func TestBuildClaudeCLIEnv_UsesDetectedGitBashOnWindows(t *testing.T) {
	env, err := buildClaudeCLIEnv(ai.ProviderConfig{}, []string{"ProgramFiles=C:\\Program Files"}, "windows", func(name string) (string, error) {
		switch name {
		case "bash.exe":
			return "", errors.New("not found")
		case "bash":
			return "", errors.New("not found")
		case "git.exe":
			return "C:\\Program Files\\Git\\cmd\\git.exe", nil
		default:
			return "", errors.New("unexpected lookup")
		}
	}, func(path string) bool {
		return path == `C:\Program Files\Git\bin\bash.exe`
	})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if got := envValue(env, "CLAUDE_CODE_GIT_BASH_PATH"); got != `C:\Program Files\Git\bin\bash.exe` {
		t.Fatalf("expected detected git bash path, got %q", got)
	}
}

func TestBuildClaudeCLIEnv_ReturnsActionableErrorWhenGitBashMissingOnWindows(t *testing.T) {
	_, err := buildClaudeCLIEnv(ai.ProviderConfig{}, []string{"ProgramFiles=C:\\Program Files"}, "windows", func(name string) (string, error) {
		return "", errors.New("not found")
	}, func(path string) bool {
		return false
	})
	if err == nil {
		t.Fatal("expected error when git bash is missing on windows")
	}
	if !strings.Contains(err.Error(), "git-bash") {
		t.Fatalf("expected git-bash hint, got %v", err)
	}
	if !strings.Contains(err.Error(), "CLAUDE_CODE_GIT_BASH_PATH") {
		t.Fatalf("expected env var hint, got %v", err)
	}
}

func TestClaudeCLIProvider_ChatTimesOutWhenCommandDoesNotFinish(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\nsleep 5\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	originalRequestTimeout := claudeCLIRequestTimeout
	claudeCLIRequestTimeout = 200 * time.Millisecond
	defer func() {
		claudeCLIRequestTimeout = originalRequestTimeout
	}()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	start := time.Now()
	_, err = provider.Chat(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	})
	if err == nil {
		t.Fatal("expected chat timeout error")
	}
	if !strings.Contains(err.Error(), "执行超时") {
		t.Fatalf("expected timeout error, got %v", err)
	}
	if time.Since(start) < 200*time.Millisecond {
		t.Fatalf("expected timeout path to wait for configured deadline, took %s", time.Since(start))
	}
}

func TestClaudeCLIProvider_ChatStreamUsesRequestTimeoutWhenNoMeaningfulResponseArrives(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\necho '{\"type\":\"system\",\"subtype\":\"init\"}'\nexec sleep 5\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	originalRequestTimeout := claudeCLIRequestTimeout
	claudeCLIRequestTimeout = 200 * time.Millisecond
	defer func() {
		claudeCLIRequestTimeout = originalRequestTimeout
	}()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	var chunks []ai.StreamChunk
	err = provider.ChatStream(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	}, func(chunk ai.StreamChunk) {
		chunks = append(chunks, chunk)
	})
	if err != nil {
		t.Fatalf("expected stream provider to report timeout via callback, got %v", err)
	}
	if len(chunks) == 0 {
		t.Fatal("expected timeout chunk")
	}
	lastChunk := chunks[len(chunks)-1]
	if !lastChunk.Done {
		t.Fatalf("expected timeout chunk to terminate stream, got %#v", lastChunk)
	}
	if !strings.Contains(lastChunk.Error, "执行超时") {
		t.Fatalf("expected request timeout message, got %#v", lastChunk)
	}
}

func TestClaudeCLIProvider_ChatStreamAllowsDelayedMeaningfulResponse(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\necho '{\"type\":\"system\",\"subtype\":\"init\"}'\nsleep 0.2\necho '{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"OK\"}]}}'\necho '{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\":\"OK\"}'\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	originalRequestTimeout := claudeCLIRequestTimeout
	claudeCLIRequestTimeout = 1 * time.Second
	defer func() {
		claudeCLIRequestTimeout = originalRequestTimeout
	}()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	var chunks []ai.StreamChunk
	err = provider.ChatStream(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	}, func(chunk ai.StreamChunk) {
		chunks = append(chunks, chunk)
	})
	if err != nil {
		t.Fatalf("expected delayed response to complete via callback, got %v", err)
	}
	if len(chunks) == 0 {
		t.Fatal("expected delayed response chunks")
	}
	if chunks[0].Content != "OK" {
		t.Fatalf("expected delayed content chunk, got %#v", chunks)
	}
	if !chunks[len(chunks)-1].Done {
		t.Fatalf("expected terminal done chunk, got %#v", chunks[len(chunks)-1])
	}
}

func TestClaudeCLIProvider_ChatReturnsErrorWhenJSONResponseIsError(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\necho '{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":true,\"result\":\"API Error: Unable to connect to API (ECONNRESET)\",\"error\":\"unknown\"}'\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	_, err = provider.Chat(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	})
	if err == nil {
		t.Fatal("expected chat error when CLI JSON marks request as failed")
	}
	if !strings.Contains(err.Error(), "Unable to connect to API") {
		t.Fatalf("expected upstream API error, got %v", err)
	}
}

func TestClaudeCLIProvider_ChatStreamReportsAssistantErrorEvent(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\necho '{\"type\":\"assistant\",\"is_error\":true,\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"API Error: Unable to connect to API (ECONNRESET)\"}]},\"error\":\"unknown\"}'\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	var chunks []ai.StreamChunk
	err = provider.ChatStream(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	}, func(chunk ai.StreamChunk) {
		chunks = append(chunks, chunk)
	})
	if err != nil {
		t.Fatalf("expected stream provider to report error via callback, got %v", err)
	}
	if len(chunks) != 1 {
		t.Fatalf("expected a single terminal error chunk, got %#v", chunks)
	}
	if chunks[0].Content != "" {
		t.Fatalf("expected assistant error event to avoid content output, got %#v", chunks[0])
	}
	if !chunks[0].Done || !strings.Contains(chunks[0].Error, "Unable to connect to API") {
		t.Fatalf("expected upstream API error chunk, got %#v", chunks[0])
	}
}

func TestClaudeCLIProvider_ChatStreamReportsResultErrorEvent(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\necho '{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":true,\"result\":\"API Error: Unable to connect to API (ECONNRESET)\",\"error\":\"unknown\"}'\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	var chunks []ai.StreamChunk
	err = provider.ChatStream(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	}, func(chunk ai.StreamChunk) {
		chunks = append(chunks, chunk)
	})
	if err != nil {
		t.Fatalf("expected stream provider to report error via callback, got %v", err)
	}
	if len(chunks) != 1 {
		t.Fatalf("expected a single terminal error chunk, got %#v", chunks)
	}
	if chunks[0].Content != "" {
		t.Fatalf("expected result error event to avoid content output, got %#v", chunks[0])
	}
	if !chunks[0].Done || !strings.Contains(chunks[0].Error, "Unable to connect to API") {
		t.Fatalf("expected upstream API error chunk, got %#v", chunks[0])
	}
}

func TestClaudeCLIProvider_ChatStreamReportsApiRetryAuthenticationFailure(t *testing.T) {
	fakeClaude := writeFakeClaudeScript(t, "#!/bin/sh\necho '{\"type\":\"system\",\"subtype\":\"api_retry\",\"attempt\":1,\"max_retries\":10,\"retry_delay_ms\":536.11,\"error_status\":401,\"error\":\"authentication_failed\",\"session_id\":\"retry-1\"}'\nexec sleep 5\n")
	restore := overrideClaudeCLIForTest(t, fakeClaude)
	defer restore()

	provider, err := NewClaudeCLIProvider(ai.ProviderConfig{
		BaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
		APIKey:  "sk-test",
		Model:   "qwen3.5-plus",
	})
	if err != nil {
		t.Fatalf("unexpected provider error: %v", err)
	}

	var chunks []ai.StreamChunk
	err = provider.ChatStream(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{Role: "user", Content: "ping"}},
	}, func(chunk ai.StreamChunk) {
		chunks = append(chunks, chunk)
	})
	if err != nil {
		t.Fatalf("expected stream provider to report authentication error via callback, got %v", err)
	}
	if len(chunks) != 1 {
		t.Fatalf("expected a single terminal error chunk, got %#v", chunks)
	}
	if !chunks[0].Done {
		t.Fatalf("expected terminal error chunk, got %#v", chunks[0])
	}
	if strings.Contains(chunks[0].Error, "未收到模型响应") {
		t.Fatalf("expected auth failure instead of startup timeout, got %#v", chunks[0])
	}
	if !strings.Contains(chunks[0].Error, "401") || !strings.Contains(chunks[0].Error, "authentication_failed") {
		t.Fatalf("expected auth retry error details, got %#v", chunks[0])
	}
}

func writeFakeClaudeScript(t *testing.T, content string) string {
	t.Helper()
	dir := t.TempDir()

	if runtime.GOOS == "windows" {
		bashPath, err := resolveClaudeCodeGitBashPath(os.Environ(), runtime.GOOS, exec.LookPath, fileExists)
		if err != nil {
			t.Fatalf("failed to resolve git bash for fake claude command: %v", err)
		}

		scriptPath := filepath.Join(dir, "claude.sh")
		if err := os.WriteFile(scriptPath, []byte(content), 0o755); err != nil {
			t.Fatalf("failed to write fake claude shell script: %v", err)
		}

		wrapperPath := filepath.Join(dir, "claude.cmd")
		wrapper := fmt.Sprintf("@echo off\r\n\"%s\" \"%s\" %%*\r\n", bashPath, scriptPath)
		if err := os.WriteFile(wrapperPath, []byte(wrapper), 0o755); err != nil {
			t.Fatalf("failed to write fake claude wrapper: %v", err)
		}
		return wrapperPath
	}

	path := filepath.Join(dir, "claude")
	if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
		t.Fatalf("failed to write fake claude script: %v", err)
	}
	return path
}

func overrideClaudeCLIForTest(t *testing.T, fakeClaudePath string) func() {
	t.Helper()

	originalLookPath := claudeLookPath
	originalCommandContext := claudeCommandContext
	claudeLookPath = func(name string) (string, error) {
		if name == "claude" {
			return fakeClaudePath, nil
		}
		return originalLookPath(name)
	}
	claudeCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd {
		if name == "claude" {
			return exec.CommandContext(ctx, fakeClaudePath, args...)
		}
		return originalCommandContext(ctx, name, args...)
	}

	originalPath := os.Getenv("PATH")
	if err := os.Setenv("PATH", filepath.Dir(fakeClaudePath)+string(os.PathListSeparator)+originalPath); err != nil {
		t.Fatalf("failed to override PATH: %v", err)
	}

	return func() {
		claudeLookPath = originalLookPath
		claudeCommandContext = originalCommandContext
		_ = os.Setenv("PATH", originalPath)
	}
}
