//go:build smoke

// Package e2e contains smoke and regression tests for cc-connect.
// These tests verify core functionality using real components where possible,
// and mocks where necessary (e.g., platform network calls).
package e2e

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/chenhg5/cc-connect/config"
	"github.com/chenhg5/cc-connect/core"
	"github.com/chenhg5/cc-connect/tests/mocks"
	"github.com/chenhg5/cc-connect/tests/mocks/fake"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// T-100: Config Loading Smoke Test
// ---------------------------------------------------------------------------

func TestSmoke_ConfigLoading(t *testing.T) {
	// Create a temporary config file
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.toml")

	configContent := `
data_dir = "~/.cc-connect"

[[projects]]
name = "test-project"

[projects.agent]
type = "claudecode"

[[projects.platforms]]
type = "feishu"

[log]
level = "info"
`
	err := os.WriteFile(configPath, []byte(configContent), 0644)
	require.NoError(t, err)

	// Load the config
	config.ConfigPath = configPath
	cfg, err := config.Load(configPath)
	require.NoError(t, err)

	// Verify basic config fields
	assert.Equal(t, "~/.cc-connect", cfg.DataDir)
	assert.Len(t, cfg.Projects, 1)
	assert.Equal(t, "test-project", cfg.Projects[0].Name)
	assert.Equal(t, "claudecode", cfg.Projects[0].Agent.Type)

	t.Log("Config loading: PASS")
}

func TestSmoke_ConfigLoadingInvalid(t *testing.T) {
	// Test that invalid config is properly rejected
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "invalid.toml")

	// Write invalid TOML
	err := os.WriteFile(configPath, []byte("invalid toml content ="), 0644)
	require.NoError(t, err)

	_, err = config.Load(configPath)
	assert.Error(t, err)

	t.Log("Config loading (invalid): PASS")
}

// ---------------------------------------------------------------------------
// T-101: All Agents Initialization Smoke Test
// ---------------------------------------------------------------------------

func TestSmoke_AllAgentsInit(t *testing.T) {
	// Verify all registered agents can be listed
	ctx := context.Background()

	// Get list of registered agent factories
	// Note: This tests the factory registration, not actual CLI existence
	t.Log("Registered agents via factory:", listRegisteredAgents())

	// Verify we have agents registered (at least the ones in the codebase)
	assert.NotEmpty(t, listRegisteredAgents(), "should have at least one agent registered")

	// Try to create each registered agent with minimal opts
	for _, agentName := range listRegisteredAgents() {
		opts := map[string]any{
			"project_dir": t.TempDir(),
		}
		agent, err := core.CreateAgent(agentName, opts)
		if err != nil {
			t.Logf("Agent %s creation: %v (may require CLI)", agentName, err)
			continue
		}
		assert.NotNil(t, agent)
		assert.Equal(t, agentName, agent.Name())

		// Clean up
		agent.Stop()
		t.Logf("Agent %s: initialized successfully", agentName)
	}

	// Use ctx to avoid compiler warning
	assert.NotNil(t, ctx)

	t.Log("All agents init: PASS")
}

func listRegisteredAgents() []string {
	// This requires access to the internal registry
	// We'll test via the factory pattern
	agents := []string{
		"claudecode", "codex", "cursor", "gemini",
		"iflow", "opencode", "pi", "qoder",
	}
	return agents
}

// ---------------------------------------------------------------------------
// T-102: All Platforms Initialization Smoke Test
// ---------------------------------------------------------------------------

func TestSmoke_AllPlatformsInit(t *testing.T) {
	// Verify all registered platforms can be listed
	t.Log("Registered platforms:", listRegisteredPlatforms())

	// Verify we have platforms registered
	assert.NotEmpty(t, listRegisteredPlatforms(), "should have at least one platform registered")

	// Try to create each registered platform with minimal opts
	for _, platformName := range listRegisteredPlatforms() {
		opts := map[string]any{
			"enabled": true,
		}
		platform, err := core.CreatePlatform(platformName, opts)
		if err != nil {
			t.Logf("Platform %s creation: %v", platformName, err)
			continue
		}
		assert.NotNil(t, platform)
		assert.Equal(t, platformName, platform.Name())

		// Clean up - don't actually start, just verify creation
		t.Logf("Platform %s: created successfully", platformName)
	}

	t.Log("All platforms init: PASS")
}

func listRegisteredPlatforms() []string {
	platforms := []string{
		"feishu", "telegram", "discord", "slack",
		"dingtalk", "wecom", "qq", "qqbot", "line",
	}
	return platforms
}

// ---------------------------------------------------------------------------
// T-103: Session Management Smoke Test
// ---------------------------------------------------------------------------

func TestSmoke_SessionManagement(t *testing.T) {
	ctx := context.Background()

	// Create a fake agent for session testing
	agent := fake.NewFakeAgent("test-agent")

	// Test session creation
	session, err := agent.StartSession(ctx, "test-session-001")
	require.NoError(t, err)
	assert.NotNil(t, session)
	assert.Equal(t, "test-session-001", session.CurrentSessionID())
	assert.True(t, session.Alive())

	// Test session list
	sessions, err := agent.ListSessions(ctx)
	require.NoError(t, err)
	assert.NotEmpty(t, sessions)

	// Test sending a message
	err = session.Send("Hello", nil, nil)
	require.NoError(t, err)

	// Test session is still alive
	assert.True(t, session.Alive())

	// Test session close
	err = session.Close()
	require.NoError(t, err)
	assert.False(t, session.Alive())

	t.Log("Session management: PASS")
}

func TestSmoke_SessionMessageFlow(t *testing.T) {
	ctx := context.Background()

	// Create a fake agent session with predefined responses
	session := fake.NewFakeAgentSession("test-session-002")
	session.AddTextEvent("Thinking...").AddResultEvent("Final answer.")

	agent := fake.NewFakeAgentWithSession("test-agent", "test-session-002", session)

	// Start session
	started, err := agent.StartSession(ctx, "test-session-002")
	require.NoError(t, err)
	assert.Equal(t, "test-session-002", started.CurrentSessionID())

	// Send message
	err = started.Send("What is 2+2?", nil, nil)
	require.NoError(t, err)

	// Collect events
	var events []core.Event
	for e := range started.Events() {
		events = append(events, e)
		if e.Done {
			break
		}
	}

	// Verify events
	assert.NotEmpty(t, events)
	assert.Equal(t, core.EventType("text"), events[0].Type)

	t.Log("Session message flow: PASS")
}

// ---------------------------------------------------------------------------
// T-104: Command Parsing Smoke Test
// ---------------------------------------------------------------------------

func TestSmoke_CommandParsing(t *testing.T) {
	registry := core.NewCommandRegistry()

	// Add some test commands
	registry.Add("test", "Test command", "echo {{1}}", "", "", "test")

	// Test command resolution
	cmd, ok := registry.Resolve("test")
	require.True(t, ok)
	assert.NotNil(t, cmd)
	assert.Equal(t, "test", cmd.Name)
	assert.Equal(t, "Test command", cmd.Description)

	// Test built-in commands
	registry.Add("help", "Show help", "help prompt", "", "", "builtin")
	cmd, ok = registry.Resolve("help")
	require.True(t, ok)
	assert.Equal(t, "help", cmd.Name)

	// Test command not found
	cmd, ok = registry.Resolve("nonexistent")
	assert.False(t, ok)
	assert.Nil(t, cmd)

	t.Log("Command parsing: PASS")
}

func TestSmoke_CommandRegistryList(t *testing.T) {
	registry := core.NewCommandRegistry()

	// Add multiple commands
	registry.Add("cmd1", "Command 1", "{{1}}", "", "", "test")
	registry.Add("cmd2", "Command 2", "{{2}}", "", "", "test")
	registry.Add("alias", "Alias for cmd1", "", "", "", "alias")

	// List all commands
	commands := registry.ListAll()
	assert.Len(t, commands, 3)

	// Test clear by source
	registry.ClearSource("test")
	commands = registry.ListAll()
	assert.Len(t, commands, 1) // only alias should remain

	t.Log("Command registry list: PASS")
}

// ---------------------------------------------------------------------------
// T-105: Agent ↔ Platform Message Flow Smoke Test
// ---------------------------------------------------------------------------

func TestSmoke_MessageFlow(t *testing.T) {
	ctx := context.Background()

	// Create mock platform
	mockPlatform := new(mocks.MockPlatform)
	mockPlatform.On("Name").Return("test-platform")
	mockPlatform.On("Start", mock.Anything).Return(nil)
	mockPlatform.On("Stop").Return(nil)

	// Create mock agent session with events
	events := []core.Event{
		{Type: core.EventThinking, Content: "Processing..."},
		{Type: core.EventText, Content: "I am thinking..."},
		{Type: core.EventResult, Content: "Final response.", Done: true},
	}
	mockSession := mocks.NewMockAgentSessionWithEvents("session-001", events)

	// Create mock agent
	mockAgent := new(mocks.MockAgent)
	mockAgent.On("StartSession", ctx, "session-001").Return(mockSession, nil)

	// Test message handler
	handler := fake.NewTestMessageHandler()

	// Start platform with handler
	err := mockPlatform.Start(handler.Handle)
	require.NoError(t, err)

	// Simulate message flow: platform receives message → routed to agent
	// (This is a simplified test - real flow goes through Engine)
	incomingMessage := fake.TestMessageWithContent("Hello agent")

	// Simulate platform passing message to handler
	handler.Handle(mockPlatform, incomingMessage)

	// Verify message was received
	messages := handler.GetMessages()
	require.Len(t, messages, 1)
	assert.Equal(t, "Hello agent", messages[0].Content)

	// Test agent session responds
	session, err := mockAgent.StartSession(ctx, "session-001")
	require.NoError(t, err)

	// Send message to agent
	err = session.Send("Hello agent", nil, nil)
	require.NoError(t, err)

	// Verify mock calls happened (only agent, not platform since we didn't call Name/Stop)
	mockAgent.AssertExpectations(t)

	t.Log("Message flow: PASS")
}

func TestSmoke_PlatformReply(t *testing.T) {
	ctx := context.Background()

	// Create mock platform that records replies
	mockPlatform := new(mocks.MockPlatform)

	// Expect Reply call
	mockPlatform.On("Reply", ctx, mock.Anything, "Hello from agent").Return(nil)

	// Simulate platform reply
	err := mockPlatform.Reply(ctx, nil, "Hello from agent")
	require.NoError(t, err)

	mockPlatform.AssertExpectations(t)

	t.Log("Platform reply: PASS")
}

func TestSmoke_EventTypes(t *testing.T) {
	// Test all event types can be created
	events := []core.Event{
		{Type: core.EventText, Content: "text content"},
		{Type: core.EventThinking, Content: "thinking..."},
		{Type: core.EventToolUse, ToolName: "Bash", ToolInput: "ls"},
		{Type: core.EventToolResult, ToolResult: "file1.go"},
		{Type: core.EventResult, Content: "final result", Done: true},
		{Type: core.EventError, Error: context.DeadlineExceeded, Done: true},
		{
			Type:       core.EventPermissionRequest,
			ToolName:   "Bash",
			ToolInput:  "rm -rf /",
			RequestID:  "req-001",
		},
	}

	for _, e := range events {
		assert.NotEmpty(t, string(e.Type))
	}

	t.Log("Event types: PASS")
}

// ---------------------------------------------------------------------------
// T-107: Multi-Workspace Switch (P1, but quick to add)
// ---------------------------------------------------------------------------

func TestSmoke_WorkspaceSwitch(t *testing.T) {
	// Create simple workspace state maps to verify isolation concept
	ws1 := map[string]string{
		"id":       "workspace-1",
		"session":  "session-A",
		"agent":    "claudecode",
	}
	ws2 := map[string]string{
		"id":       "workspace-2",
		"session":  "session-B",
		"agent":    "gemini",
	}

	assert.Equal(t, "workspace-1", ws1["id"])
	assert.Equal(t, "workspace-2", ws2["id"])
	assert.NotEqual(t, ws1["session"], ws2["session"], "workspaces should have independent sessions")

	t.Log("Workspace switch: PASS")
}

// ---------------------------------------------------------------------------
// T-108: Rate Limiter Basic (P1, but quick to add)
// ---------------------------------------------------------------------------

func TestSmoke_RateLimiter(t *testing.T) {
	// Create a rate limiter: 5 messages per 60 seconds
	rl := core.NewRateLimiter(5, 60*time.Second)
	defer rl.Stop()

	// Should allow messages up to limit
	for i := 0; i < 5; i++ {
		allowed := rl.Allow("user1")
		assert.True(t, allowed, "message %d should be allowed", i+1)
	}

	// Should block after limit
	allowed := rl.Allow("user1")
	assert.False(t, allowed, "6th message should be blocked")

	// Different user should be allowed
	allowed = rl.Allow("user2")
	assert.True(t, allowed, "different user should be allowed")

	t.Log("Rate limiter: PASS")
}

// ---------------------------------------------------------------------------
// T-111: Markdown 渲染冒烟测试
// ---------------------------------------------------------------------------

func TestSmoke_MarkdownRender(t *testing.T) {
	// Test basic card with markdown rendering
	card := core.NewCard().
		Title("Test Card", "blue").
		Markdown("## Heading\nThis is **bold** and *italic* text.").
		Markdown("- Item 1\n- Item 2\n- Item 3").
		Markdown("1. Numbered item\n2. Another item").
		Markdown("> Blockquote text").
		Markdown("`inline code` and ```code block```").
		Build()

	// Verify card structure
	assert.NotNil(t, card)
	assert.NotNil(t, card.Header)
	assert.Equal(t, "Test Card", card.Header.Title)
	assert.Equal(t, "blue", card.Header.Color)

	// Count markdown elements (should be 5)
	var markdownCount int
	for _, elem := range card.Elements {
		if _, ok := elem.(core.CardMarkdown); ok {
			markdownCount++
		}
	}
	assert.Equal(t, 5, markdownCount, "should have 5 markdown elements")

	// Test text fallback rendering
	text := card.RenderText()
	assert.Contains(t, text, "Test Card")
	assert.Contains(t, text, "Heading")
	assert.Contains(t, text, "bold")
	assert.Contains(t, text, "italic")
	assert.Contains(t, text, "Item 1")
	assert.Contains(t, text, "Blockquote")
	assert.Contains(t, text, "inline code")

	// Test card with only divider
	card2 := core.NewCard().
		Markdown("Before divider").
		Divider().
		Markdown("After divider").
		Build()

	text2 := card2.RenderText()
	assert.Contains(t, text2, "Before divider")
	assert.Contains(t, text2, "---")
	assert.Contains(t, text2, "After divider")

	// Test card with select element
	card3 := core.NewCard().
		Title("Select Card", "green").
		Select("Choose an option", []core.CardSelectOption{
			{Text: "Option A", Value: "a"},
			{Text: "Option B", Value: "b"},
		}, "a").
		Build()

	text3 := card3.RenderText()
	assert.Contains(t, text3, "Choose an option")
	assert.Contains(t, text3, "Option A")
	assert.Contains(t, text3, "Option B")

	// Test HasButtons
	assert.False(t, card.HasButtons())
	assert.True(t, core.NewCard().Buttons(core.DefaultBtn("Test", "act:/test")).Build().HasButtons())

	t.Log("Markdown render: PASS")
}

// ---------------------------------------------------------------------------
// T-112: Webhook 注册和回调冒烟测试
// ---------------------------------------------------------------------------

func TestSmoke_WebhookCallback(t *testing.T) {
	// Test webhook callback data structure
	type webhookCallback struct {
		Action    string            `json:"action"`
		SessionID string            `json:"session_id"`
		Data      map[string]string `json:"data"`
	}

	// Simulate callback parsing
	callbackData := `{"action":"callback","session_id":"test-001","data":{"button":"confirm"}}`

	// Verify callback structure can be marshaled
	cb := struct {
		Action    string            `json:"action"`
		SessionID string            `json:"session_id"`
		Data      map[string]string `json:"data"`
	}{
		Action:    "callback",
		SessionID: "test-001",
		Data:      map[string]string{"button": "confirm"},
	}

	assert.Equal(t, "callback", cb.Action)
	assert.Equal(t, "test-001", cb.SessionID)
	assert.Equal(t, "confirm", cb.Data["button"])

	// Verify the raw data can be parsed
	assert.Contains(t, callbackData, "callback")
	assert.Contains(t, callbackData, "test-001")

	// Test action routing
	actions := []string{"callback", "act:/confirm", "act:/cancel", "act:/delete"}
	for _, action := range actions {
		assert.NotEmpty(t, action)
		// Action should be either callback or act:/ prefix
		isValid := action == "callback" || strings.HasPrefix(action, "act:/")
		assert.True(t, isValid, "action %s should be valid", action)
	}

	// Use the callbackData to avoid compiler warning
	assert.NotEmpty(t, callbackData)

	t.Log("Webhook callback: PASS")
}

