//go:build integration

package integration

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/chenhg5/cc-connect/core"
	"github.com/stretchr/testify/require"
)

const integrationSharedAgentName = "integration-shared-routing-agent"

var (
	registerIntegrationSharedAgentOnce sync.Once
	integrationMessageSeq              uint64
)

type integrationRoutingAgent struct {
	workDir string
}

func (a *integrationRoutingAgent) Name() string { return integrationSharedAgentName }

func (a *integrationRoutingAgent) StartSession(_ context.Context, sessionID string) (core.AgentSession, error) {
	return newIntegrationRoutingSession(sessionID, a.workDir), nil
}

func (a *integrationRoutingAgent) ListSessions(_ context.Context) ([]core.AgentSessionInfo, error) {
	return nil, nil
}

func (a *integrationRoutingAgent) Stop() error { return nil }

type integrationRoutingSession struct {
	mu        sync.RWMutex
	sessionID string
	workDir   string
	alive     bool
	events    chan core.Event
}

func newIntegrationRoutingSession(sessionID, workDir string) *integrationRoutingSession {
	return &integrationRoutingSession{
		sessionID: sessionID,
		workDir:   workDir,
		alive:     true,
		events:    make(chan core.Event, 8),
	}
}

func (s *integrationRoutingSession) Send(prompt string, _ []core.ImageAttachment, _ []core.FileAttachment) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if !s.alive {
		return io.ErrClosedPipe
	}

	s.events <- core.Event{
		Type:    core.EventResult,
		Content: fmt.Sprintf("workspace=%s prompt=%s", s.workDir, prompt),
		Done:    true,
	}
	return nil
}

func (s *integrationRoutingSession) RespondPermission(string, core.PermissionResult) error {
	return nil
}

func (s *integrationRoutingSession) Events() <-chan core.Event {
	return s.events
}

func (s *integrationRoutingSession) CurrentSessionID() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.sessionID
}

func (s *integrationRoutingSession) Alive() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.alive
}

func (s *integrationRoutingSession) Close() error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.alive {
		s.alive = false
		close(s.events)
	}
	return nil
}

type integrationPlatform struct {
	mu           sync.Mutex
	name         string
	channelNames map[string]string
	handler      core.MessageHandler
	outputs      []string
}

func newIntegrationPlatform(name string, channelNames map[string]string) *integrationPlatform {
	return &integrationPlatform{
		name:         name,
		channelNames: channelNames,
		outputs:      make([]string, 0),
	}
}

func (p *integrationPlatform) Name() string { return p.name }

func (p *integrationPlatform) Start(handler core.MessageHandler) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.handler = handler
	return nil
}

func (p *integrationPlatform) Reply(_ context.Context, _ any, content string) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.outputs = append(p.outputs, content)
	return nil
}

func (p *integrationPlatform) Send(_ context.Context, _ any, content string) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.outputs = append(p.outputs, content)
	return nil
}

func (p *integrationPlatform) Stop() error { return nil }

func (p *integrationPlatform) ResolveChannelName(channelID string) (string, error) {
	if name, ok := p.channelNames[channelID]; ok {
		return name, nil
	}
	return "", fmt.Errorf("unknown channel %q", channelID)
}

func (p *integrationPlatform) Emit(msg *core.Message) {
	p.mu.Lock()
	handler := p.handler
	p.mu.Unlock()
	if handler == nil {
		panic("integrationPlatform.Emit called before Start")
	}
	handler(p, msg)
}

func (p *integrationPlatform) ClearOutputs() {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.outputs = p.outputs[:0]
}

func (p *integrationPlatform) Outputs() []string {
	p.mu.Lock()
	defer p.mu.Unlock()
	cp := make([]string, len(p.outputs))
	copy(cp, p.outputs)
	return cp
}

func (p *integrationPlatform) WaitForOutputContaining(t *testing.T, needle string) string {
	t.Helper()
	var matched string
	require.Eventually(t, func() bool {
		for _, output := range p.Outputs() {
			if strings.Contains(output, needle) {
				matched = output
				return true
			}
		}
		return false
	}, 3*time.Second, 20*time.Millisecond)
	return matched
}

func registerIntegrationSharedAgent() {
	registerIntegrationSharedAgentOnce.Do(func() {
		core.RegisterAgent(integrationSharedAgentName, func(opts map[string]any) (core.Agent, error) {
			workDir, _ := opts["work_dir"].(string)
			return &integrationRoutingAgent{workDir: workDir}, nil
		})
	})
}

func newIntegrationEngine(t *testing.T, projectName string, platform *integrationPlatform, baseDir, bindingStore, sessionStore string) *core.Engine {
	t.Helper()
	registerIntegrationSharedAgent()

	engine := core.NewEngine(
		projectName,
		&integrationRoutingAgent{workDir: baseDir},
		[]core.Platform{platform},
		sessionStore,
		core.LangEnglish,
	)
	engine.SetAdminFrom("admin")
	engine.SetMultiWorkspace(baseDir, bindingStore)
	require.NoError(t, engine.Start())
	t.Cleanup(func() {
		_ = engine.Stop()
	})
	return engine
}

func integrationMessage(platformName, channelID, userID, content string) *core.Message {
	seq := atomic.AddUint64(&integrationMessageSeq, 1)
	chatName := channelID
	return &core.Message{
		SessionKey: fmt.Sprintf("%s:%s:%s", platformName, channelID, userID),
		Platform:   platformName,
		MessageID:  fmt.Sprintf("msg-%d", seq),
		UserID:     userID,
		UserName:   userID,
		ChatName:   chatName,
		Content:    content,
		ReplyCtx:   fmt.Sprintf("reply-%d", seq),
	}
}

func TestIntegration_SharedWorkspaceBindingLiveSyncAcrossProjects(t *testing.T) {
	baseDir := t.TempDir()
	bindingStore := filepath.Join(t.TempDir(), "workspace_bindings.json")
	channelID := "shared-channel"
	channelNames := map[string]string{channelID: "shared-channel"}
	platformName := "shared-platform"

	sharedDir := filepath.Join(baseDir, "shared-workspace")
	require.NoError(t, os.MkdirAll(sharedDir, 0o755))

	platformA := newIntegrationPlatform(platformName, channelNames)
	engineA := newIntegrationEngine(t, "project-a", platformA, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-a-sessions.json"))
	_ = engineA

	platformB := newIntegrationPlatform(platformName, channelNames)
	engineB := newIntegrationEngine(t, "project-b", platformB, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-b-sessions.json"))
	_ = engineB

	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "/workspace shared bind shared-workspace"))
	platformA.WaitForOutputContaining(t, "Shared workspace bound")

	platformA.ClearOutputs()
	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "hello from project a"))
	platformA.WaitForOutputContaining(t, sharedDir)

	platformB.ClearOutputs()
	platformB.Emit(integrationMessage(platformB.Name(), channelID, "user-b", "hello from project b"))
	platformB.WaitForOutputContaining(t, sharedDir)

	platformA.ClearOutputs()
	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "/workspace shared unbind"))
	platformA.WaitForOutputContaining(t, "Shared workspace unbound")

	platformB.ClearOutputs()
	platformB.Emit(integrationMessage(platformB.Name(), channelID, "user-b", "after shared unbind"))
	platformB.WaitForOutputContaining(t, "No workspace found for this channel")
}

func TestIntegration_ProjectWorkspaceOverridesSharedAcrossProjects(t *testing.T) {
	baseDir := t.TempDir()
	bindingStore := filepath.Join(t.TempDir(), "workspace_bindings.json")
	channelID := "override-channel"
	channelNames := map[string]string{channelID: "override-channel"}
	platformName := "shared-platform"

	sharedDir := filepath.Join(baseDir, "shared-workspace")
	projectBDir := filepath.Join(baseDir, "project-b-workspace")
	require.NoError(t, os.MkdirAll(sharedDir, 0o755))
	require.NoError(t, os.MkdirAll(projectBDir, 0o755))

	platformA := newIntegrationPlatform(platformName, channelNames)
	engineA := newIntegrationEngine(t, "project-a", platformA, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-a-sessions.json"))
	_ = engineA

	platformB := newIntegrationPlatform(platformName, channelNames)
	engineB := newIntegrationEngine(t, "project-b", platformB, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-b-sessions.json"))
	_ = engineB

	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "/workspace shared bind shared-workspace"))
	platformA.WaitForOutputContaining(t, "Shared workspace bound")

	platformB.ClearOutputs()
	platformB.Emit(integrationMessage(platformB.Name(), channelID, "user-b", "/workspace bind project-b-workspace"))
	platformB.WaitForOutputContaining(t, "Workspace bound")

	platformA.ClearOutputs()
	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "route project a"))
	platformA.WaitForOutputContaining(t, sharedDir)

	platformB.ClearOutputs()
	platformB.Emit(integrationMessage(platformB.Name(), channelID, "user-b", "route project b"))
	platformB.WaitForOutputContaining(t, projectBDir)
}

func TestIntegration_ProjectWorkspaceRouteUsesAbsolutePath(t *testing.T) {
	baseDir := t.TempDir()
	bindingStore := filepath.Join(t.TempDir(), "workspace_bindings.json")
	channelID := "route-channel"
	channelNames := map[string]string{channelID: "route-channel"}

	routedDir := filepath.Join(t.TempDir(), "routed workspace")
	require.NoError(t, os.MkdirAll(routedDir, 0o755))

	platform := newIntegrationPlatform("proj-route-platform", channelNames)
	engine := newIntegrationEngine(t, "project-route", platform, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-route-sessions.json"))
	_ = engine

	platform.Emit(integrationMessage(platform.Name(), channelID, "user-route", "/workspace route "+routedDir))
	platform.WaitForOutputContaining(t, "Workspace routed")

	platform.ClearOutputs()
	platform.Emit(integrationMessage(platform.Name(), channelID, "user-route", "hello routed workspace"))
	platform.WaitForOutputContaining(t, routedDir)
}

func TestIntegration_SharedWorkspaceRouteLiveSyncAcrossProjects(t *testing.T) {
	baseDir := t.TempDir()
	bindingStore := filepath.Join(t.TempDir(), "workspace_bindings.json")
	channelID := "shared-route-channel"
	channelNames := map[string]string{channelID: "shared-route-channel"}
	platformName := "shared-platform"

	routedDir := filepath.Join(t.TempDir(), "shared routed workspace")
	require.NoError(t, os.MkdirAll(routedDir, 0o755))

	platformA := newIntegrationPlatform(platformName, channelNames)
	engineA := newIntegrationEngine(t, "project-shared-route-a", platformA, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-shared-route-a-sessions.json"))
	_ = engineA

	platformB := newIntegrationPlatform(platformName, channelNames)
	engineB := newIntegrationEngine(t, "project-shared-route-b", platformB, baseDir, bindingStore, filepath.Join(t.TempDir(), "project-shared-route-b-sessions.json"))
	_ = engineB

	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "/workspace shared route "+routedDir))
	platformA.WaitForOutputContaining(t, "Shared workspace routed")

	platformA.ClearOutputs()
	platformA.Emit(integrationMessage(platformA.Name(), channelID, "user-a", "hello from shared route a"))
	platformA.WaitForOutputContaining(t, routedDir)

	platformB.ClearOutputs()
	platformB.Emit(integrationMessage(platformB.Name(), channelID, "user-b", "hello from shared route b"))
	platformB.WaitForOutputContaining(t, routedDir)
}
