package operator

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agentview"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/desktop/internal/daemon"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

func TestAPIAgentOperatorPreservesSnapshots(t *testing.T) {
	t.Parallel()

	want := agentview.Snapshot{
		ID:                "test-agent",
		Name:              "Test Agent",
		Runtime:           "codex",
		Status:            "running",
		BackendRegistered: true,
		Source:            "agentteam:agentteam-channel",
		SourceType:        "agentteam",
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/api/internal/agents":
			if err := json.NewEncoder(w).Encode([]agentview.Snapshot{want}); err != nil {
				t.Errorf("encode agent list: %v", err)
			}
		case "/api/internal/agents/test-agent":
			if err := json.NewEncoder(w).Encode(want); err != nil {
				t.Errorf("encode agent detail: %v", err)
			}
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	agents := newAPIOperator(server.URL, "test-key").Agents()
	listed, err := agents.All()
	require.NoError(t, err)
	require.Equal(t, []agentview.Snapshot{want}, listed)

	fetched, ok, err := agents.Get("test-agent")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, want, fetched)
}

func TestChannelFromResponse(t *testing.T) {
	resp := dto.ServerResponse{
		ID:      "test-ch",
		Enabled: true,
		Name:    "Test AgentTeam",
		AgentID: "agent-1",
		Config:  map[string]interface{}{"server_url": "https://agentteam.example.com", "api_key": "secret"},
	}

	got := serverFromResponse(resp)

	if got.ID != "test-ch" {
		t.Errorf("ID = %q, want %q", got.ID, "test-ch")
	}
	if got.AgentID != "agent-1" {
		t.Errorf("AgentID = %q, want %q", got.AgentID, "agent-1")
	}
	if len(got.Config) == 0 {
		t.Error("Config should not be empty")
	}
}

func TestChannelFromResponse_EmptyConfig(t *testing.T) {
	resp := dto.ServerResponse{
		ID: "empty-ch",
	}

	got := serverFromResponse(resp)
	if len(got.Config) != 0 {
		t.Errorf("Config should be empty, got %v", got.Config)
	}
}

func TestChannelToRequest(t *testing.T) {
	ch := profile.AgentTeamServer{
		ID:      "req-ch",
		Enabled: true,
		Name:    "AgentTeam Channel",
		AgentID: "agent-2",
		Config:  types.RawMessage(`server_url: "http://localhost:8080"` + "\n" + `api_key: "key"` + "\n"),
	}

	got := serverToRequest(ch)

	if got.ID != "req-ch" {
		t.Errorf("ID = %q, want %q", got.ID, "req-ch")
	}
	if got.Config == nil {
		t.Fatal("Config should not be nil")
	}
	if got.Config["server_url"] != "http://localhost:8080" {
		t.Errorf("Config[server_url] = %v, want http://localhost:8080", got.Config["server_url"])
	}
}

func TestSkillFromResponse(t *testing.T) {
	now := time.Now().UTC().Format(time.RFC3339)

	resp := dto.SkillResponse{
		ID:          "code-review",
		Name:        "Code Review",
		Description: "Review code quality",
		Version:     "1.0",
		Author:      "team",
		Tags:        []string{"review", "quality"},
		FileName:    "code-review.md",
		CreatedAt:   &now,
	}

	got := skillFromResponse(resp)

	if got.ID != "code-review" {
		t.Errorf("ID = %q, want %q", got.ID, "code-review")
	}
	if got.Name != "Code Review" {
		t.Errorf("Name = %q, want %q", got.Name, "Code Review")
	}
	if len(got.Tags) != 2 {
		t.Errorf("Tags length = %d, want 2", len(got.Tags))
	}
	if got.CreatedAt == nil {
		t.Fatal("CreatedAt should not be nil")
	}
}

func TestSkillFromResponse_NilCreatedAt(t *testing.T) {
	resp := dto.SkillResponse{ID: "no-date", Name: "No Date"}
	got := skillFromResponse(resp)
	if got.CreatedAt != nil {
		t.Error("CreatedAt should be nil")
	}
}

func TestSkillToUpdateRequest(t *testing.T) {
	now := time.Now().UTC()
	s := profile.Skill{
		ID:          "update-skill",
		Name:        "Updated Skill",
		Description: "Updated desc",
		Tags:        []string{"tag1"},
		FileName:    "updated.md",
		CreatedAt:   &now,
	}

	got := skillToUpdateRequest(s)

	if got.ID != "update-skill" {
		t.Errorf("ID = %q, want %q", got.ID, "update-skill")
	}
	if got.CreatedAt == nil {
		t.Fatal("CreatedAt should not be nil")
	}
}

func TestSkillToUpdateRequest_JSONOmitsEnabledField(t *testing.T) {
	req := skillToUpdateRequest(profile.Skill{
		ID:       "skill-1",
		Name:     "Skill One",
		FileName: "skill-1/SKILL.md",
	})

	body, err := json.Marshal(req)
	require.NoError(t, err)
	require.NotContains(t, string(body), `"enabled"`)
}

func TestTranslateError(t *testing.T) {
	tests := []struct {
		name       string
		statusCode int
		body       string
		wantMsg    string
	}{
		{
			name:       "error with message",
			statusCode: 400,
			body:       `{"error": "bad request"}`,
			wantMsg:    "bad request",
		},
		{
			name:       "error without message",
			statusCode: 500,
			body:       `{"something": "else"}`,
			wantMsg:    "daemon error (HTTP 500)",
		},
		{
			name:       "empty body",
			statusCode: 502,
			body:       ``,
			wantMsg:    "daemon error (HTTP 502)",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := translateError(tt.statusCode, []byte(tt.body))
			if err == nil {
				t.Fatal("expected error, got nil")
			}
			if err.Error() != tt.wantMsg {
				t.Errorf("Error() = %q, want %q", err.Error(), tt.wantMsg)
			}
		})
	}
}

func TestIsNotFound(t *testing.T) {
	err404 := translateError(404, []byte(`{"error": "not found"}`))
	err500 := translateError(500, []byte(`{"error": "internal"}`))

	if !isNotFound(err404) {
		t.Error("isNotFound(404) should be true")
	}
	if isNotFound(err500) {
		t.Error("isNotFound(500) should be false")
	}
	if isNotFound(errors.New("other")) {
		t.Error("isNotFound(other error) should be false")
	}
}

func TestIsConflict(t *testing.T) {
	err409 := translateError(409, []byte(`{"error": "conflict"}`))
	if !isConflict(err409) {
		t.Error("isConflict(409) should be true")
	}
}

func TestIsUnprocessable(t *testing.T) {
	err422 := translateError(422, []byte(`{"error": "unprocessable"}`))
	if !isUnprocessable(err422) {
		t.Error("isUnprocessable(422) should be true")
	}
}

func TestAPIError_Error(t *testing.T) {
	err := &apiError{StatusCode: 403, Message: "forbidden"}
	if err.Error() != "forbidden" {
		t.Errorf("Error() = %q, want %q", err.Error(), "forbidden")
	}
}

func TestIsAPIOperator(t *testing.T) {
	op := newAPIOperator("http://localhost:8080", "test-key")
	if !IsAPIOperator(op) {
		t.Error("IsAPIOperator(apiOperator) should be true")
	}

	fileOp := newFileOperator()
	if IsAPIOperator(fileOp) {
		t.Error("IsAPIOperator(fileOperator) should be false")
	}
}

func TestAsAPIOperator(t *testing.T) {
	op := newAPIOperator("http://localhost:8080", "test-key")
	apiOp := AsAPIOperator(op)
	if apiOp == nil {
		t.Fatal("AsAPIOperator(apiOperator) should not be nil")
	}
	if apiOp.baseURL != "http://localhost:8080" {
		t.Errorf("baseURL = %q, want %q", apiOp.baseURL, "http://localhost:8080")
	}
	if apiOp.apiKey != "test-key" {
		t.Errorf("apiKey = %q, want %q", apiOp.apiKey, "test-key")
	}

	fileOp := newFileOperator()
	if AsAPIOperator(fileOp) != nil {
		t.Error("AsAPIOperator(fileOperator) should be nil")
	}
}

func TestAPIOperator_BaseURL_APIKey(t *testing.T) {
	op := newAPIOperator("http://localhost:9090", "my-key").(*apiOperator)
	if op.BaseURL() != "http://localhost:9090" {
		t.Errorf("BaseURL() = %q, want %q", op.BaseURL(), "http://localhost:9090")
	}
	if op.APIKey() != "my-key" {
		t.Errorf("APIKey() = %q, want %q", op.APIKey(), "my-key")
	}
}

func mustEncodeJSON(t *testing.T, w http.ResponseWriter, payload any) {
	t.Helper()
	if err := json.NewEncoder(w).Encode(payload); err != nil {
		t.Fatalf("encode JSON response: %v", err)
	}
}

func mustDecodeJSON(t *testing.T, r *http.Request, payload any) {
	t.Helper()
	if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
		t.Fatalf("decode JSON request: %v", err)
	}
}

func TestAPIOperator_AgentReadOnly_MockServer(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/api/internal/agents":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, []agentview.Snapshot{
				{ID: "test-agent", Name: "Test", Runtime: "opencode", Source: "agentteam"},
			})
		case r.Method == http.MethodGet && r.URL.Path == "/api/internal/agents/test-agent":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, agentview.Snapshot{
				ID: "test-agent", Name: "Test", Runtime: "opencode",
				Source: "agentteam",
			})
		default:
			w.WriteHeader(http.StatusNotFound)
			mustEncodeJSON(t, w, map[string]string{"error": "not found"})
		}
	}))
	defer server.Close()

	op := newAPIOperator(server.URL, "test-key")

	agents, err := op.Agents().All()
	if err != nil {
		t.Fatalf("All() error = %v", err)
	}
	if len(agents) != 1 {
		t.Fatalf("All() length = %d, want 1", len(agents))
	}

	a, ok, err := op.Agents().Get("test-agent")
	if err != nil || !ok {
		t.Fatalf("Get() error = %v, ok = %v", err, ok)
	}
	if a.Runtime != "opencode" {
		t.Errorf("Get() Runtime = %q", a.Runtime)
	}
}

func TestAPIOperator_ChannelCRUD_MockServer(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/api/internal/servers":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, []dto.ServerResponse{})
		case r.Method == http.MethodPost && r.URL.Path == "/api/internal/servers":
			var req dto.ServerRequest
			mustDecodeJSON(t, r, &req)
			chID := "ch_abc123"
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusCreated)
			mustEncodeJSON(t, w, dto.ServerResponse{
				ID: chID, Name: req.Name,
				AgentID: req.AgentID, Config: req.Config,
			})
		case r.Method == http.MethodGet && r.URL.Path == "/api/internal/servers/ch_abc123":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, dto.ServerResponse{
				ID: "ch_abc123", Name: "Test AgentTeam",
				AgentID: "agent-1",
			})
		case r.Method == http.MethodDelete && r.URL.Path == "/api/internal/servers/ch_abc123":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, map[string]string{"status": "deleted"})
		default:
			w.WriteHeader(http.StatusNotFound)
			mustEncodeJSON(t, w, map[string]string{"error": "not found"})
		}
	}))
	defer server.Close()

	op := newAPIOperator(server.URL, "test-key")

	ch, err := op.Servers().Add(&profile.AgentTeamServer{
		Name: "Test AgentTeam", AgentID: "agent-1",
		Config: types.RawMessage(`server_url: "http://localhost"` + "\n"),
	})
	if err != nil {
		t.Fatalf("Add() error = %v", err)
	}
	if ch.ID != "ch_abc123" {
		t.Errorf("Add() ID = %q, want ch_abc123", ch.ID)
	}

	ch, ok, err := op.Servers().Get("ch_abc123")
	if err != nil || !ok {
		t.Fatalf("Get() error = %v, ok = %v", err, ok)
	}
	if err := op.Servers().Delete("ch_abc123"); err != nil {
		t.Fatalf("Delete() error = %v", err)
	}
}

func TestAPIOperator_SkillCRUD_MockServer(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/api/internal/skills":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, []dto.SkillResponse{})
		case r.Method == http.MethodPut && r.URL.Path == "/api/internal/skills/test-skill":
			var req dto.SkillResponse
			mustDecodeJSON(t, r, &req)
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, dto.SkillResponse{
				ID: req.ID, Name: req.Name,
			})
		case r.Method == http.MethodGet && r.URL.Path == "/api/internal/skills/test-skill":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, dto.SkillResponse{
				ID: "test-skill", Name: "Test Skill",
			})
		case r.Method == http.MethodDelete && r.URL.Path == "/api/internal/skills/test-skill":
			w.Header().Set("Content-Type", "application/json")
			mustEncodeJSON(t, w, map[string]string{"status": "deleted"})
		default:
			w.WriteHeader(http.StatusNotFound)
			mustEncodeJSON(t, w, map[string]string{"error": "not found"})
		}
	}))
	defer server.Close()

	op := newAPIOperator(server.URL, "test-key")

	skills, err := op.Skills().All()
	if err != nil {
		t.Fatalf("All() error = %v", err)
	}
	if len(skills) != 0 {
		t.Errorf("All() = %+v, want empty", skills)
	}

	s, ok, err := op.Skills().Get("test-skill")
	if err != nil || !ok {
		t.Fatalf("Get() error = %v, ok = %v", err, ok)
	}
	if s.Name != "Test Skill" {
		t.Errorf("Get() Name = %q", s.Name)
	}

	s, ok, err = op.Skills().Get("test-skill")
	if err != nil || !ok {
		t.Fatalf("Get() for Update error = %v, ok = %v", err, ok)
	}
	s.Description = "updated description"
	if _, err := op.Skills().Update(s); err != nil {
		t.Fatalf("Update() error = %v", err)
	}

	if err := op.Skills().Delete("test-skill"); err != nil {
		t.Fatalf("Delete() error = %v", err)
	}
}

func TestAPIOperator_SendsAuthHeader(t *testing.T) {
	var receivedKey string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		receivedKey = r.Header.Get("X-Api-Key")
		w.Header().Set("Content-Type", "application/json")
		mustEncodeJSON(t, w, []agentview.Snapshot{})
	}))
	defer server.Close()

	op := newAPIOperator(server.URL, "sk-dev-test-key-12345")
	_, err := op.Agents().All()
	if err != nil {
		t.Fatalf("All() error = %v", err)
	}
	if receivedKey != "sk-dev-test-key-12345" {
		t.Errorf("X-Api-Key header = %q, want %q", receivedKey, "sk-dev-test-key-12345")
	}
}

func TestNewUsesCLIControlKeyWhenDaemonIsRunning(t *testing.T) {
	originalDaemonStatus := daemonStatus
	originalGenerateControlKey := generateCLIControlKey
	t.Cleanup(func() {
		daemonStatus = originalDaemonStatus
		generateCLIControlKey = originalGenerateControlKey
	})

	daemonStatus = func() *daemon.Status {
		return &daemon.Status{
			Running: true,
			Healthy: true,
			PID:     12345,
			BaseURL: "http://127.0.0.1:49123",
		}
	}

	called := false
	generateCLIControlKey = func() (string, error) {
		called = true
		return "sk-tmp-test-key-12345", nil
	}

	op, err := New()
	require.NoError(t, err)
	require.True(t, called)

	apiOp, ok := op.(*apiOperator)
	require.True(t, ok)
	require.Equal(t, "sk-tmp-test-key-12345", apiOp.apiKey)
	require.Equal(t, "http://127.0.0.1:49123", apiOp.baseURL)
}

func TestNewFailsClosedWhenDaemonIsRunningButUnhealthy(t *testing.T) {
	originalDaemonStatus := daemonStatus
	t.Cleanup(func() {
		daemonStatus = originalDaemonStatus
	})

	daemonStatus = func() *daemon.Status {
		return &daemon.Status{Running: true, Healthy: false, PID: 12345}
	}

	op, err := New()
	require.Nil(t, op)
	require.ErrorContains(t, err, "running but unavailable")
}

func TestNewUsesFileOperatorOnlyWhenDaemonIsNotRunning(t *testing.T) {
	originalDaemonStatus := daemonStatus
	t.Cleanup(func() {
		daemonStatus = originalDaemonStatus
	})

	daemonStatus = func() *daemon.Status {
		return &daemon.Status{Running: false}
	}

	op, err := New()
	require.NoError(t, err)
	_, ok := op.(*fileOperator)
	require.True(t, ok)
}

func TestAPIOperator_ServerError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
		mustEncodeJSON(t, w, map[string]string{"error": "internal server error"})
	}))
	defer server.Close()

	op := newAPIOperator(server.URL, "test-key")
	_, err := op.Agents().All()
	if err == nil {
		t.Fatal("expected error for 500 response, got nil")
	}
	if err.Error() != "internal server error" {
		t.Errorf("error = %q, want %q", err.Error(), "internal server error")
	}
}

func TestAPIOperator_ServerUnreachable(t *testing.T) {
	op := newAPIOperator("http://127.0.0.1:1", "test-key")
	_, err := op.Agents().All()
	if err == nil {
		t.Fatal("expected error for unreachable server, got nil")
	}
}
