package profile

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/provider"
)

func TestReadRuntimeConfigCodex(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	envKey := "AGENTTEAM_TEST_CODEX_CRS_OAI_KEY"
	t.Setenv(envKey, "")
	os.Unsetenv(envKey)
	runtimeconfig.SetShellEnvForRuntimeResolution(map[string]string{
		envKey: "sk-codex",
	})
	t.Cleanup(func() {
		runtimeconfig.SetShellEnvForRuntimeResolution(nil)
	})

	codexDir := filepath.Join(home, ".codex")
	if err := os.MkdirAll(codexDir, 0o755); err != nil {
		t.Fatal(err)
	}
	config := `model_provider = "crs"
model = "gpt-5.4"
model_reasoning_effort = "high"

[model_providers.crs]
name = "CRS Proxy"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
env_key = "` + envKey + `"
`
	if err := os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}

	got, err := ReadRuntimeConfig("codex")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if got.Runtime != "codex" {
		t.Fatalf("runtime = %q, want codex", got.Runtime)
	}
	if !got.Exists {
		t.Fatal("expected config to exist")
	}
	if !strings.HasSuffix(got.ConfigPath, ".codex/config.toml") {
		t.Fatalf("config path = %q", got.ConfigPath)
	}
	if got.Editable.BaseURL != "https://proxy.example.com/v1" {
		t.Fatalf("base url = %q", got.Editable.BaseURL)
	}
	if got.Editable.APIKeyEnvKey != envKey {
		t.Fatalf("env key = %q, want %q", got.Editable.APIKeyEnvKey, envKey)
	}
	if !got.Editable.APIKeyConfigured {
		t.Fatal("expected api key to be marked configured")
	}
	if got.Editable.Model != "gpt-5.4" {
		t.Fatalf("model = %q, want gpt-5.4", got.Editable.Model)
	}
	if got.Editable.ReasoningEffort != "high" {
		t.Fatalf("reasoning effort = %q, want high", got.Editable.ReasoningEffort)
	}
	if got.Editable.WireAPI != provider.WireAPIResponses {
		t.Fatalf("wire api = %q, want %q", got.Editable.WireAPI, provider.WireAPIResponses)
	}
	if !got.Offline.OK {
		t.Fatalf("expected offline validation to pass, got %+v", got.Offline)
	}
}

func TestReadRuntimeConfigOpenCodeUsesNativeConfigDetails(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	configDir := filepath.Join(home, ".config", "opencode")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	config := `{
  "model": "agentteam/gpt-5.4",
  "provider": {
    "agentteam": {
      "name": "AgentTeam Proxy",
      "options": {
        "baseURL": "https://proxy.example.com/v1",
        "apiKey": "sk-inline"
      },
      "models": {
        "gpt-5.4": {}
      }
    }
  }
}`
	if err := os.WriteFile(filepath.Join(configDir, "opencode.json"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}

	got, err := ReadRuntimeConfig("opencode")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if got.Editable.BaseURL != "https://proxy.example.com/v1" {
		t.Fatalf("base url = %q", got.Editable.BaseURL)
	}
	if !got.Editable.APIKeyConfigured {
		t.Fatal("expected inline api key to be marked configured")
	}
	if got.Editable.Model != "gpt-5.4" {
		t.Fatalf("model = %q, want gpt-5.4", got.Editable.Model)
	}
}

func TestReadRuntimeConfigOpenCodePreservesOptionsWireAPI(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	configDir := filepath.Join(home, ".config", "opencode")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	config := `{
  "model": "agentteam/gpt-5.4",
  "provider": {
    "agentteam": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "AgentTeam Proxy",
      "options": {
        "baseURL": "https://proxy.example.com/v1",
        "apiKey": "sk-inline",
        "wireAPI": "responses"
      },
      "models": {
        "gpt-5.4": {}
      }
    }
  }
}`
	if err := os.WriteFile(filepath.Join(configDir, "opencode.json"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}

	got, err := ReadRuntimeConfig("opencode")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if got.Editable.WireAPI != provider.WireAPIResponses {
		t.Fatalf("wire api = %q, want %q", got.Editable.WireAPI, provider.WireAPIResponses)
	}
}

func TestReadRuntimeConfigOpenCodeInfersResponsesFromOpenAINPM(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	configDir := filepath.Join(home, ".config", "opencode")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	config := `{
  "model": "agentteam/gpt-5.4",
  "provider": {
    "agentteam": {
      "npm": "@ai-sdk/openai",
      "name": "AgentTeam Proxy",
      "options": {
        "baseURL": "https://proxy.example.com/v1",
        "apiKey": "sk-inline"
      },
      "models": {
        "gpt-5.4": {}
      }
    }
  }
}`
	if err := os.WriteFile(filepath.Join(configDir, "opencode.json"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}

	got, err := ReadRuntimeConfig("opencode")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if got.Editable.WireAPI != provider.WireAPIResponses {
		t.Fatalf("wire api = %q, want %q", got.Editable.WireAPI, provider.WireAPIResponses)
	}
}

func TestReadRuntimeConfigClaudePreservesAuthTokenEnvKey(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	claudeDir := filepath.Join(home, ".claude")
	if err := os.MkdirAll(claudeDir, 0o755); err != nil {
		t.Fatal(err)
	}
	config := `{
  "env": {
    "ANTHROPIC_BASE_URL": "https://dendro.codgician.me/anthropic",
    "ANTHROPIC_AUTH_TOKEN": "Bearer dendro-token",
    "ANTHROPIC_MODEL": "claude-opus-4.6"
  }
}
`
	if err := os.WriteFile(filepath.Join(claudeDir, "settings.json"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}

	got, err := ReadRuntimeConfig("claude")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if got.Editable.APIKey != "Bearer dendro-token" {
		t.Fatalf("api key = %q, want bearer token", got.Editable.APIKey)
	}
	if got.Editable.APIKeyEnvKey != "ANTHROPIC_AUTH_TOKEN" {
		t.Fatalf("api key env key = %q, want ANTHROPIC_AUTH_TOKEN", got.Editable.APIKeyEnvKey)
	}
}

func runtimeConfigTestDir(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()

	defaultMu.Lock()
	oldConfigDir := configDir
	oldConfigPath := configPath
	oldDefaultManager := defaultManager
	oldDefaultDir := defaultDir
	configDir = dir
	configPath = ""
	defaultManager = nil
	defaultDir = ""
	defaultMu.Unlock()

	t.Cleanup(func() {
		defaultMu.Lock()
		configDir = oldConfigDir
		configPath = oldConfigPath
		defaultManager = oldDefaultManager
		defaultDir = oldDefaultDir
		defaultMu.Unlock()
	})

	return dir
}

func TestBuiltinRuntimeConfigReadsOpenCodeConfig(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	opencodeDir := filepath.Join(home, ".config", "opencode")
	if err := os.MkdirAll(opencodeDir, 0o755); err != nil {
		t.Fatal(err)
	}
	config := `{
  "model": "agentteam/gpt-4",
  "provider": {
    "agentteam": {
      "name": "AgentTeam Proxy",
      "options": {
        "baseURL": "https://api.example.com",
        "apiKey": "sk-test"
      },
      "models": {
        "gpt-4": {}
      }
    }
  }
}
`
	if err := os.WriteFile(filepath.Join(opencodeDir, "opencode.json"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}

	read, err := ReadRuntimeConfig("builtin")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if read.Runtime != "opencode" {
		t.Fatalf("runtime = %q, want opencode", read.Runtime)
	}
	if !read.Exists {
		t.Fatal("expected opencode runtime config to be marked available")
	}
	if read.Editable.BaseURL != "https://api.example.com" {
		t.Fatalf("base url = %q", read.Editable.BaseURL)
	}
	if read.Editable.APIKey != "sk-test" {
		t.Fatalf("api key = %q", read.Editable.APIKey)
	}
	if read.Editable.Model != "gpt-4" {
		t.Fatalf("model = %q", read.Editable.Model)
	}
	if !read.Offline.OK {
		t.Fatalf("expected offline validation to pass, got %+v", read.Offline)
	}
}

func TestSetLLMConfigCanClearExistingConfig(t *testing.T) {
	cfgDir := runtimeConfigTestDir(t)

	mgr := New(cfgDir)
	if err := mgr.SetLLMConfig(LLM{BaseURL: "https://api.example.com/v1", APIKey: "sk-test", Model: "gpt-4"}); err != nil {
		t.Fatalf("SetLLMConfig initial: %v", err)
	}
	if err := mgr.SetLLMConfig(LLM{}); err != nil {
		t.Fatalf("SetLLMConfig clear: %v", err)
	}

	cfg, err := Load(filepath.Join(cfgDir, "config.yaml"))
	if err != nil {
		t.Fatalf("Load saved config: %v", err)
	}
	if cfg.LLM != (LLM{}) {
		t.Fatalf("saved LLM = %+v, want zero value", cfg.LLM)
	}
	if got := mgr.Config().LLM; got != (LLM{}) {
		t.Fatalf("manager Config().LLM = %+v, want zero value", got)
	}
}

func TestBuiltinRuntimeConfigFallsBackToOpenCodeConfig(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	read, err := ReadRuntimeConfig("builtin")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if read.Runtime != "opencode" {
		t.Fatalf("runtime = %q, want opencode", read.Runtime)
	}
	if read.Exists {
		t.Fatal("expected missing opencode settings file")
	}
	if read.Offline.OK || !slices.Contains(read.Offline.Missing, "apiKey") || !slices.Contains(read.Offline.Missing, "model") {
		t.Fatalf("expected offline validation to require apiKey and model, got %+v", read.Offline)
	}
}

func TestValidateBuiltinRuntimeConfigUsesOpenCodeModelEndpoint(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	requestedPath := ""
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestedPath = r.URL.Path
		if r.Header.Get("Authorization") != "Bearer sk-test" {
			http.Error(w, "missing auth", http.StatusUnauthorized)
			return
		}
		if r.URL.Path != "/models" {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"data":[{"id":"model-b"},{"id":"model-a"}]}`))
	}))
	defer server.Close()

	if _, err := UpdateRuntimeConfig("builtin", RuntimeConfigEditable{
		BaseURL: server.URL,
		APIKey:  "sk-test",
	}); err != nil {
		t.Fatalf("UpdateRuntimeConfig: %v", err)
	}

	got, err := ValidateRuntimeConfig("builtin", RuntimeConfigEditable{})
	if err != nil {
		t.Fatalf("ValidateRuntimeConfig: %v", err)
	}
	if !got.Online.OK {
		t.Fatalf("expected online model detection to pass, got %+v", got.Online)
	}
	if requestedPath != "/models" {
		t.Fatalf("requested path = %q, want /models", requestedPath)
	}
	if len(got.Online.Models) != 2 || got.Online.Models[0] != "model-a" || got.Online.Models[1] != "model-b" {
		t.Fatalf("models = %+v, want sorted model IDs", got.Online.Models)
	}
}

func TestValidateBuiltinRuntimeConfigReturnsFriendlyReasonForRejectedAPIKey(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "forbidden", http.StatusForbidden)
	}))
	defer server.Close()

	if _, err := UpdateRuntimeConfig("builtin", RuntimeConfigEditable{
		BaseURL: server.URL,
		APIKey:  "sk-test",
	}); err != nil {
		t.Fatalf("UpdateRuntimeConfig: %v", err)
	}

	got, err := ValidateRuntimeConfig("builtin", RuntimeConfigEditable{})
	if err != nil {
		t.Fatalf("ValidateRuntimeConfig: %v", err)
	}
	if got.Online.OK {
		t.Fatalf("expected online validation to fail, got %+v", got.Online)
	}
	if got.Online.ReasonCode != provider.ModelFetchReasonAPIKeyRejected {
		t.Fatalf("reason code = %q, want %q", got.Online.ReasonCode, provider.ModelFetchReasonAPIKeyRejected)
	}
}

func TestValidateBuiltinRuntimeConfigFallsBackWhenModelsEndpointUnavailable(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.NotFound(w, r)
	}))
	defer server.Close()

	if _, err := UpdateRuntimeConfig("builtin", RuntimeConfigEditable{
		BaseURL: server.URL,
		APIKey:  "sk-test",
	}); err != nil {
		t.Fatalf("UpdateRuntimeConfig: %v", err)
	}

	got, err := ValidateRuntimeConfig("builtin", RuntimeConfigEditable{})
	if err != nil {
		t.Fatalf("ValidateRuntimeConfig: %v", err)
	}
	if !got.Online.OK {
		t.Fatalf("expected online validation to return fallback success, got %+v", got.Online)
	}
	if got.Online.ReasonCode != "models_unavailable" {
		t.Fatalf("reason code = %q, want models_unavailable", got.Online.ReasonCode)
	}
	if len(got.Online.Models) != 0 {
		t.Fatalf("models = %+v, want no detected models", got.Online.Models)
	}
}

func TestBuiltinRuntimeConfigUpdateWritesOpenCodeConfig(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	_, err := UpdateRuntimeConfig("builtin", RuntimeConfigEditable{
		BaseURL: "https://api.new.com",
		APIKey:  "sk-new",
		Model:   "gpt-5",
	})
	if err != nil {
		t.Fatalf("UpdateRuntimeConfig: %v", err)
	}

	read, err := ReadRuntimeConfig("opencode")
	if err != nil {
		t.Fatalf("ReadRuntimeConfig: %v", err)
	}
	if read.Editable.BaseURL != "https://api.new.com" {
		t.Fatalf("base url = %q", read.Editable.BaseURL)
	}
	if read.Editable.APIKey != "sk-new" {
		t.Fatalf("api key = %q", read.Editable.APIKey)
	}
	if read.Editable.Model != "gpt-5" {
		t.Fatalf("model = %q", read.Editable.Model)
	}
}

func TestBuiltinRuntimeConfigUpdateWritesOpenCodeNPMForResponses(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	_, err := UpdateRuntimeConfig("builtin", RuntimeConfigEditable{
		BaseURL: "https://api.new.com/v1",
		APIKey:  "sk-new",
		Model:   "gpt-5",
		WireAPI: provider.WireAPIResponses,
	})
	if err != nil {
		t.Fatalf("UpdateRuntimeConfig: %v", err)
	}

	data, err := os.ReadFile(filepath.Join(home, ".config", "opencode", "opencode.jsonc"))
	if err != nil {
		t.Fatal(err)
	}
	var config openCodeConfig
	if err := json.Unmarshal(data, &config); err != nil {
		t.Fatal(err)
	}
	prov, ok := config.Provider[internalRuntimeProviderID("opencode")]
	if !ok {
		t.Fatalf("missing opencode provider in config: %+v", config.Provider)
	}
	if prov.NPM != "@ai-sdk/openai" {
		t.Fatalf("npm = %q, want @ai-sdk/openai", prov.NPM)
	}
	if prov.Options == nil || prov.Options.WireAPI != provider.WireAPIResponses {
		t.Fatalf("wire api options = %+v, want %q", prov.Options, provider.WireAPIResponses)
	}
}

func TestUpdateRuntimeConfigCodexIsReadOnly(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	codexDir := filepath.Join(home, ".codex")
	if err := os.MkdirAll(codexDir, 0o755); err != nil {
		t.Fatal(err)
	}
	configPath := filepath.Join(codexDir, "config.toml")
	config := `model_provider = "crs"
model = "gpt-5.4"
model_reasoning_effort = "high"
approval_policy = "never"

[model_providers.crs]
name = "CRS Proxy"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
env_key = "CRS_OAI_KEY"
`
	if err := os.WriteFile(configPath, []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}
	original, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatal(err)
	}

	_, err = UpdateRuntimeConfig("codex", RuntimeConfigEditable{
		BaseURL:      "https://new-proxy.example.com/v1",
		APIKeyEnvKey: defaultCodexEnvKey,
		Model:        "gpt-5.5",
	})
	if !errors.Is(err, ErrRuntimeConfigReadOnly) {
		t.Fatalf("UpdateRuntimeConfig error = %v, want ErrRuntimeConfigReadOnly", err)
	}
	data, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != string(original) {
		t.Fatalf("codex config changed; before:\n%s\nafter:\n%s", original, data)
	}
}
