package dashboard

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

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

func TestCheckRuntimeHandlerTreatsCodexBinaryAsAvailable(t *testing.T) {
	codexPath := writeRuntimeBinary(t, "codex")
	t.Setenv("PATH", filepath.Dir(codexPath))
	restoreCodexProbe(t, true)

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.GET("/runtimes/:name/available", NewCheckRuntimeHandler())

	req := httptest.NewRequest(http.MethodGet, "/runtimes/codex/available", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var resp struct {
		Type      string `json:"type"`
		Binary    string `json:"binary"`
		Available bool   `json:"available"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
	require.Equal(t, "codex", resp.Type)
	require.Equal(t, "codex", resp.Binary)
	require.True(t, resp.Available)
}

func TestCheckRuntimeHandlerTreatsCodexWithoutAppServerAsUnavailable(t *testing.T) {
	codexPath := writeRuntimeBinary(t, "codex")
	t.Setenv("PATH", filepath.Dir(codexPath))
	restoreCodexProbe(t, false)

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.GET("/runtimes/:name/available", NewCheckRuntimeHandler())

	req := httptest.NewRequest(http.MethodGet, "/runtimes/codex/available", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var resp struct {
		Type      string `json:"type"`
		Binary    string `json:"binary"`
		Available bool   `json:"available"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
	require.Equal(t, "codex", resp.Type)
	require.Equal(t, "codex", resp.Binary)
	require.False(t, resp.Available)
}

func TestRuntimeAvailabilityUsesCodexCapabilityProbe(t *testing.T) {
	codexPath := writeRuntimeBinary(t, "codex")
	t.Setenv("PATH", filepath.Dir(codexPath))
	restoreCodexProbe(t, false)

	binary, available := runtimeAvailability(runtimeconfig.TypeCodex)
	require.Equal(t, "codex", binary)
	require.False(t, available)
}

func TestRuntimeAvailabilityTreatsBuiltinAsManagedOpenCode(t *testing.T) {
	opencodePath := writeRuntimeBinary(t, "opencode")
	writeBundledOpenCodeModels(t, opencodePath)
	t.Setenv("AGENTTEAM_MANAGED_OPENCODE_PATH", opencodePath)
	oldManagedVersion := runtimeconfig.ManagedOpenCodeVersion
	t.Cleanup(func() { runtimeconfig.ManagedOpenCodeVersion = oldManagedVersion })
	runtimeconfig.ManagedOpenCodeVersion = ""

	binary, available := runtimeAvailability(runtimeconfig.TypeBuiltin)
	require.Equal(t, "opencode", binary)
	require.True(t, available)
}

func TestGetRuntimeConfigHandlerReturnsSnapshot(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	t.Setenv("CRS_OAI_KEY", "sk-runtime")

	codexDir := filepath.Join(home, ".codex")
	require.NoError(t, os.MkdirAll(codexDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(`model_provider = "crs"
model = "gpt-5.4"

[model_providers.crs]
name = "CRS"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
env_key = "CRS_OAI_KEY"
`), 0o644))

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.GET("/runtimes/:name/config", NewGetRuntimeConfigHandler())

	req := httptest.NewRequest(http.MethodGet, "/runtimes/codex/config", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var resp struct {
		Runtime  string `json:"runtime"`
		Exists   bool   `json:"exists"`
		Editable struct {
			BaseURL          string `json:"baseUrl"`
			APIKey           string `json:"apiKey"`
			Model            string `json:"model"`
			APIKeyConfigured bool   `json:"apiKeyConfigured"`
		} `json:"editable"`
		Offline struct {
			OK bool `json:"ok"`
		} `json:"offline"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
	require.Equal(t, "codex", resp.Runtime)
	require.True(t, resp.Exists)
	require.Equal(t, "https://proxy.example.com/v1", resp.Editable.BaseURL)
	require.Empty(t, resp.Editable.APIKey)
	require.Equal(t, "gpt-5.4", resp.Editable.Model)
	require.True(t, resp.Editable.APIKeyConfigured)
	require.True(t, resp.Offline.OK)
}

func writeRuntimeBinary(t *testing.T, name string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), name)
	writeRuntimeBinaryInDir(t, filepath.Dir(path), name)
	return path
}

func writeRuntimeBinaryInDir(t *testing.T, dir string, name string) string {
	t.Helper()
	path := filepath.Join(dir, name)
	require.NoError(t, os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755))
	return path
}

func writeBundledOpenCodeModels(t *testing.T, opencodePath string) {
	t.Helper()
	require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(opencodePath), "models.json"), []byte(`{"local":{"id":"local","name":"Local","env":[],"models":{}}}`+"\n"), 0o644))
}

func restoreCodexProbe(t *testing.T, available bool) {
	t.Helper()
	oldProbe := runtimeconfig.CodexAppServerAvailable
	t.Cleanup(func() { runtimeconfig.CodexAppServerAvailable = oldProbe })
	runtimeconfig.CodexAppServerAvailable = func(string) bool { return available }
}

func TestValidateRuntimeConfigHandlerReturnsOfflineResult(t *testing.T) {
	t.Setenv("HOME", t.TempDir())
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.POST("/runtimes/:name/config/validate", NewValidateRuntimeConfigHandler())

	req := httptest.NewRequest(http.MethodPost, "/runtimes/codex/config/validate", bytes.NewBufferString(`{"model":"gpt-5.4"}`))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var resp struct {
		Offline struct {
			OK      bool     `json:"ok"`
			Missing []string `json:"missing"`
		} `json:"offline"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
	require.False(t, resp.Offline.OK)
	require.Contains(t, resp.Offline.Missing, "apiKey")
}

func TestUpdateRuntimeConfigHandlerRejectsCodexConfigMutation(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	codexDir := filepath.Join(home, ".codex")
	require.NoError(t, os.MkdirAll(codexDir, 0o755))
	configPath := filepath.Join(codexDir, "config.toml")
	require.NoError(t, os.WriteFile(configPath, []byte(`model_provider = "crs"
model = "gpt-5.4"
approval_policy = "never"

[model_providers.crs]
name = "CRS"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
env_key = "CRS_OAI_KEY"
`), 0o644))
	original, err := os.ReadFile(configPath)
	require.NoError(t, err)

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.PUT("/runtimes/:name/config", NewUpdateRuntimeConfigHandler())

	req := httptest.NewRequest(http.MethodPut, "/runtimes/codex/config", bytes.NewBufferString(`{"model":"gpt-5.5","baseUrl":"https://new-proxy.example.com/v1","apiKeyEnvKey":"AGENTTEAM_CODEX_API_KEY"}`))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusBadRequest, rec.Code)
	require.Contains(t, rec.Body.String(), "runtime config is read-only")
	data, err := os.ReadFile(configPath)
	require.NoError(t, err)
	require.Equal(t, string(original), string(data))
}
