package service

import (
	"context"
	"errors"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// MockProjectRepository implements port.ProjectRepository for testing.
type MockProjectRepository struct {
	projects     map[domain.ProjectID]*domain.Project
	refreshCalls int
	refreshErr   error
}

func NewMockProjectRepository() *MockProjectRepository {
	return &MockProjectRepository{
		projects: make(map[domain.ProjectID]*domain.Project),
	}
}

func (m *MockProjectRepository) List(ctx context.Context) ([]domain.Project, error) {
	result := make([]domain.Project, 0, len(m.projects))
	for _, p := range m.projects {
		result = append(result, *p)
	}
	return result, nil
}

func (m *MockProjectRepository) Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error) {
	p, ok := m.projects[id]
	if !ok {
		return nil, domain.ErrProjectNotFound
	}
	return p, nil
}

func (m *MockProjectRepository) Exists(ctx context.Context, id domain.ProjectID) (bool, error) {
	_, ok := m.projects[id]
	return ok, nil
}

func (m *MockProjectRepository) RefreshStatus(ctx context.Context) error {
	m.refreshCalls++
	return m.refreshErr
}

func (m *MockProjectRepository) Register(ctx context.Context, p *domain.Project) error {
	m.projects[p.ID] = p
	return nil
}

func (m *MockProjectRepository) Unregister(ctx context.Context, id domain.ProjectID) error {
	delete(m.projects, id)
	return nil
}

// MockCommandExecutor implements port.CommandExecutor for testing.
// Uses atomic counters to safely track calls from concurrent goroutines.
type MockCommandExecutor struct {
	executeCalls atomic.Int32
	cancelCalls  atomic.Int32
	mu           sync.RWMutex // protects result and err
	result       *domain.CommandResult
	err          error
}

func (m *MockCommandExecutor) Execute(ctx context.Context, cmd *domain.Command, podName string, handler domain.OutputHandler) (*domain.CommandResult, error) {
	m.executeCalls.Add(1)
	m.mu.RLock()
	defer m.mu.RUnlock()
	if m.result != nil {
		return m.result, m.err
	}
	return &domain.CommandResult{
		CommandID:  cmd.ID,
		ExitCode:   0,
		DurationMs: 100,
	}, m.err
}

func (m *MockCommandExecutor) Cancel(ctx context.Context, cmdID domain.CommandID) error {
	m.cancelCalls.Add(1)
	return nil
}

func (m *MockCommandExecutor) PodExists(ctx context.Context, podName string) (bool, error) {
	return true, nil
}

func (m *MockCommandExecutor) CheckConnection(ctx context.Context) error {
	return nil
}

// ExecuteCallCount returns the number of Execute calls (thread-safe).
func (m *MockCommandExecutor) ExecuteCallCount() int {
	return int(m.executeCalls.Load())
}

// CancelCallCount returns the number of Cancel calls (thread-safe).
func (m *MockCommandExecutor) CancelCallCount() int {
	return int(m.cancelCalls.Load())
}

// SetResult sets the mock result (thread-safe).
func (m *MockCommandExecutor) SetResult(result *domain.CommandResult, err error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.result = result
	m.err = err
}

// MockStreamPublisher implements port.StreamPublisher for testing.
// Uses mutex to safely handle concurrent publishes from background goroutines.
type MockStreamPublisher struct {
	mu      sync.RWMutex
	streams map[string][]port.StreamEvent
}

func NewMockStreamPublisher() *MockStreamPublisher {
	return &MockStreamPublisher{
		streams: make(map[string][]port.StreamEvent),
	}
}

func (m *MockStreamPublisher) Subscribe(streamID string) (<-chan port.StreamEvent, func()) {
	ch := make(chan port.StreamEvent, 100)
	return ch, func() { close(ch) }
}

func (m *MockStreamPublisher) SubscribeFromID(streamID, lastEventID string) (<-chan port.StreamEvent, func()) {
	return m.Subscribe(streamID)
}

func (m *MockStreamPublisher) Publish(streamID string, event port.StreamEvent) string {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.streams[streamID] = append(m.streams[streamID], event)
	return "event-1"
}

func (m *MockStreamPublisher) Close(streamID string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.streams, streamID)
}

// GetEvents returns events for a stream (thread-safe).
func (m *MockStreamPublisher) GetEvents(streamID string) []port.StreamEvent {
	m.mu.RLock()
	defer m.mu.RUnlock()
	events := make([]port.StreamEvent, len(m.streams[streamID]))
	copy(events, m.streams[streamID])
	return events
}

func TestProjectService_List(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.Register(context.Background(), &domain.Project{ID: "proj-a", Name: "Project A"})
	repo.Register(context.Background(), &domain.Project{ID: "proj-b", Name: "Project B"})

	svc := NewProjectService(repo, nil, nil)

	projects, err := svc.List(context.Background())
	if err != nil {
		t.Fatalf("List() error = %v", err)
	}

	if len(projects) != 2 {
		t.Errorf("List() returned %d projects, want 2", len(projects))
	}

	// Should call RefreshStatus
	if repo.refreshCalls != 1 {
		t.Errorf("RefreshStatus() called %d times, want 1", repo.refreshCalls)
	}
}

func TestProjectService_List_RefreshError(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.refreshErr = errors.New("refresh failed")
	repo.Register(context.Background(), &domain.Project{ID: "proj-a", Name: "Project A"})

	svc := NewProjectService(repo, nil, nil)

	// Should still return projects even if refresh fails
	projects, err := svc.List(context.Background())
	if err != nil {
		t.Fatalf("List() error = %v", err)
	}

	if len(projects) != 1 {
		t.Errorf("List() returned %d projects, want 1", len(projects))
	}
}

func TestProjectService_Get(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.Register(context.Background(), &domain.Project{
		ID:      "my-project",
		Name:    "My Project",
		PodName: "pod-0",
	})

	svc := NewProjectService(repo, nil, nil)

	t.Run("existing project", func(t *testing.T) {
		project, err := svc.Get(context.Background(), "my-project")
		if err != nil {
			t.Fatalf("Get() error = %v", err)
		}

		if project.Name != "My Project" {
			t.Errorf("Name = %q, want %q", project.Name, "My Project")
		}
	})

	t.Run("non-existent project", func(t *testing.T) {
		_, err := svc.Get(context.Background(), "unknown")
		if !errors.Is(err, domain.ErrProjectNotFound) {
			t.Errorf("Get() error = %v, want %v", err, domain.ErrProjectNotFound)
		}
	})
}

func TestProjectService_Exists(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.Register(context.Background(), &domain.Project{ID: "existing"})

	svc := NewProjectService(repo, nil, nil)

	tests := []struct {
		id   domain.ProjectID
		want bool
	}{
		{"existing", true},
		{"unknown", false},
	}

	for _, tt := range tests {
		exists, err := svc.Exists(context.Background(), tt.id)
		if err != nil {
			t.Errorf("Exists(%q) error = %v", tt.id, err)
		}
		if exists != tt.want {
			t.Errorf("Exists(%q) = %v, want %v", tt.id, exists, tt.want)
		}
	}
}

func TestProjectService_ExecuteClaude(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.Register(context.Background(), &domain.Project{
		ID:      "my-project",
		PodName: "pod-0",
	})

	executor := &MockCommandExecutor{}
	streams := NewMockStreamPublisher()

	svc := NewProjectService(repo, executor, streams)

	t.Run("valid request", func(t *testing.T) {
		result, err := svc.ExecuteClaude(context.Background(), ExecuteClaudeRequest{
			ProjectID: "my-project",
			Prompt:    "Hello Claude",
		})
		if err != nil {
			t.Fatalf("ExecuteClaude() error = %v", err)
		}

		if result.CommandID == "" {
			t.Error("CommandID should not be empty")
		}
		if result.StreamURL == "" {
			t.Error("StreamURL should not be empty")
		}

		// Wait a bit for background goroutine
		time.Sleep(50 * time.Millisecond)

		if executor.ExecuteCallCount() != 1 {
			t.Errorf("Execute() called %d times, want 1", executor.ExecuteCallCount())
		}
	})

	t.Run("empty prompt", func(t *testing.T) {
		_, err := svc.ExecuteClaude(context.Background(), ExecuteClaudeRequest{
			ProjectID: "my-project",
			Prompt:    "",
		})
		if !errors.Is(err, domain.ErrInvalidCommand) {
			t.Errorf("ExecuteClaude() error = %v, want %v", err, domain.ErrInvalidCommand)
		}
	})

	t.Run("non-existent project", func(t *testing.T) {
		_, err := svc.ExecuteClaude(context.Background(), ExecuteClaudeRequest{
			ProjectID: "unknown",
			Prompt:    "Hello",
		})
		if !errors.Is(err, domain.ErrProjectNotFound) {
			t.Errorf("ExecuteClaude() error = %v, want %v", err, domain.ErrProjectNotFound)
		}
	})

	t.Run("custom stream ID", func(t *testing.T) {
		result, err := svc.ExecuteClaude(context.Background(), ExecuteClaudeRequest{
			ProjectID: "my-project",
			Prompt:    "Hello",
			StreamID:  "custom-stream-123",
		})
		if err != nil {
			t.Fatalf("ExecuteClaude() error = %v", err)
		}

		if result.CommandID != "custom-stream-123" {
			t.Errorf("CommandID = %q, want %q", result.CommandID, "custom-stream-123")
		}
	})
}

func TestProjectService_ExecuteShell(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.Register(context.Background(), &domain.Project{
		ID:      "my-project",
		PodName: "pod-0",
	})

	executor := &MockCommandExecutor{}
	streams := NewMockStreamPublisher()

	svc := NewProjectService(repo, executor, streams)

	t.Run("valid request", func(t *testing.T) {
		result, err := svc.ExecuteShell(context.Background(), ExecuteShellRequest{
			ProjectID: "my-project",
			Command:   "ls -la",
		})
		if err != nil {
			t.Fatalf("ExecuteShell() error = %v", err)
		}

		if result.CommandID == "" {
			t.Error("CommandID should not be empty")
		}
	})

	t.Run("empty command", func(t *testing.T) {
		_, err := svc.ExecuteShell(context.Background(), ExecuteShellRequest{
			ProjectID: "my-project",
			Command:   "",
		})
		if !errors.Is(err, domain.ErrInvalidCommand) {
			t.Errorf("ExecuteShell() error = %v, want %v", err, domain.ErrInvalidCommand)
		}
	})

	t.Run("dangerous command rejected", func(t *testing.T) {
		_, err := svc.ExecuteShell(context.Background(), ExecuteShellRequest{
			ProjectID: "my-project",
			Command:   "rm -rf /",
		})
		if !errors.Is(err, domain.ErrCommandSanitization) {
			t.Errorf("ExecuteShell() error = %v, want %v", err, domain.ErrCommandSanitization)
		}
	})
}

func TestProjectService_ExecuteGit(t *testing.T) {
	repo := NewMockProjectRepository()
	repo.Register(context.Background(), &domain.Project{
		ID:      "my-project",
		PodName: "pod-0",
	})

	executor := &MockCommandExecutor{}
	streams := NewMockStreamPublisher()

	svc := NewProjectService(repo, executor, streams)

	t.Run("valid request", func(t *testing.T) {
		result, err := svc.ExecuteGit(context.Background(), ExecuteGitRequest{
			ProjectID: "my-project",
			Args:      []string{"status"},
		})
		if err != nil {
			t.Fatalf("ExecuteGit() error = %v", err)
		}

		if result.CommandID == "" {
			t.Error("CommandID should not be empty")
		}
	})

	t.Run("empty args", func(t *testing.T) {
		_, err := svc.ExecuteGit(context.Background(), ExecuteGitRequest{
			ProjectID: "my-project",
			Args:      []string{},
		})
		if !errors.Is(err, domain.ErrInvalidCommand) {
			t.Errorf("ExecuteGit() error = %v, want %v", err, domain.ErrInvalidCommand)
		}
	})
}

func TestProjectService_Subscribe(t *testing.T) {
	streams := NewMockStreamPublisher()
	svc := NewProjectService(nil, nil, streams)

	ch, cleanup := svc.Subscribe("test-stream")
	defer cleanup()

	if ch == nil {
		t.Error("Subscribe() returned nil channel")
	}
}

func TestProjectService_SubscribeFromID(t *testing.T) {
	streams := NewMockStreamPublisher()
	svc := NewProjectService(nil, nil, streams)

	ch, cleanup := svc.SubscribeFromID("test-stream", "last-event-123")
	defer cleanup()

	if ch == nil {
		t.Error("SubscribeFromID() returned nil channel")
	}
}
