package port_test

import (
	"context"
	"testing"

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

// =============================================================================
// Interface Compliance Tests
//
// These tests verify that mock implementations can satisfy the port interfaces.
// They serve as compile-time verification that interfaces are correctly defined
// and provide example implementations for testing.
// =============================================================================

// Compile-time interface compliance checks
var (
	_ port.ProjectRepository = (*mockProjectRepository)(nil)
	_ port.CommandExecutor   = (*mockCommandExecutor)(nil)
	_ port.APIKeyRepository  = (*mockAPIKeyRepository)(nil)
	_ port.StreamPublisher   = (*mockStreamPublisher)(nil)
)

// =============================================================================
// Mock Implementations
// =============================================================================

type mockProjectRepository struct {
	projects map[domain.ProjectID]*domain.Project
}

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) {
	if p, ok := m.projects[id]; ok {
		return p, nil
	}
	return nil, domain.ErrProjectNotFound
}

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

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

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

func (m *mockProjectRepository) RefreshStatus(ctx context.Context) error {
	return nil
}

type mockCommandExecutor struct {
	executeFunc func(ctx context.Context, cmd *domain.Command, podName string, handler domain.OutputHandler) (*domain.CommandResult, error)
}

func (m *mockCommandExecutor) Execute(ctx context.Context, cmd *domain.Command, podName string, handler domain.OutputHandler) (*domain.CommandResult, error) {
	if m.executeFunc != nil {
		return m.executeFunc(ctx, cmd, podName, handler)
	}
	return &domain.CommandResult{
		CommandID:  cmd.ID,
		ExitCode:   0,
		DurationMs: 100,
	}, nil
}

func (m *mockCommandExecutor) Cancel(ctx context.Context, cmdID domain.CommandID) error {
	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
}

type mockAPIKeyRepository struct {
	keys map[domain.APIKeyID]*domain.APIKey
}

func newMockAPIKeyRepository() *mockAPIKeyRepository {
	return &mockAPIKeyRepository{
		keys: make(map[domain.APIKeyID]*domain.APIKey),
	}
}

func (m *mockAPIKeyRepository) Create(ctx context.Context, key *domain.APIKey, keyHash string) error {
	m.keys[key.ID] = key
	return nil
}

func (m *mockAPIKeyRepository) GetByHash(ctx context.Context, keyHash string) (*domain.APIKey, error) {
	// In a real implementation, this would look up by hash
	return nil, domain.ErrKeyNotFound
}

func (m *mockAPIKeyRepository) Get(ctx context.Context, id domain.APIKeyID) (*domain.APIKey, error) {
	if k, ok := m.keys[id]; ok {
		return k, nil
	}
	return nil, domain.ErrKeyNotFound
}

func (m *mockAPIKeyRepository) List(ctx context.Context) ([]*domain.APIKey, error) {
	result := make([]*domain.APIKey, 0, len(m.keys))
	for _, k := range m.keys {
		result = append(result, k)
	}
	return result, nil
}

func (m *mockAPIKeyRepository) Revoke(ctx context.Context, id domain.APIKeyID) error {
	if _, ok := m.keys[id]; !ok {
		return domain.ErrKeyNotFound
	}
	return nil
}

func (m *mockAPIKeyRepository) UpdateLastUsed(ctx context.Context, id domain.APIKeyID) error {
	if _, ok := m.keys[id]; !ok {
		return domain.ErrKeyNotFound
	}
	return nil
}

type mockStreamPublisher struct {
	subscribers map[string][]chan port.StreamEvent
}

func newMockStreamPublisher() *mockStreamPublisher {
	return &mockStreamPublisher{
		subscribers: make(map[string][]chan port.StreamEvent),
	}
}

func (m *mockStreamPublisher) Subscribe(streamID string) (<-chan port.StreamEvent, func()) {
	ch := make(chan port.StreamEvent, 10)
	m.subscribers[streamID] = append(m.subscribers[streamID], ch)
	cleanup := func() {
		close(ch)
	}
	return ch, cleanup
}

func (m *mockStreamPublisher) SubscribeFromID(streamID string, lastEventID string) (<-chan port.StreamEvent, func()) {
	// Simplified: just subscribe without replay
	return m.Subscribe(streamID)
}

func (m *mockStreamPublisher) Publish(streamID string, event port.StreamEvent) string {
	for _, ch := range m.subscribers[streamID] {
		select {
		case ch <- event:
		default:
			// Channel full, skip
		}
	}
	return event.ID
}

func (m *mockStreamPublisher) Close(streamID string) {
	for _, ch := range m.subscribers[streamID] {
		close(ch)
	}
	delete(m.subscribers, streamID)
}

// =============================================================================
// Mock Usage Tests
// =============================================================================

func TestMockProjectRepository_BasicOperations(t *testing.T) {
	repo := newMockProjectRepository()
	ctx := context.Background()

	// Register a project
	project := &domain.Project{
		ID:     "test-proj",
		Name:   "Test Project",
		Status: domain.ProjectStatusRunning,
	}

	if err := repo.Register(ctx, project); err != nil {
		t.Fatalf("Register failed: %v", err)
	}

	// Verify it exists
	exists, err := repo.Exists(ctx, "test-proj")
	if err != nil {
		t.Fatalf("Exists failed: %v", err)
	}
	if !exists {
		t.Error("project should exist after registration")
	}

	// Get the project
	got, err := repo.Get(ctx, "test-proj")
	if err != nil {
		t.Fatalf("Get failed: %v", err)
	}
	if got.Name != "Test Project" {
		t.Errorf("Got name %q, want %q", got.Name, "Test Project")
	}

	// List projects
	list, err := repo.List(ctx)
	if err != nil {
		t.Fatalf("List failed: %v", err)
	}
	if len(list) != 1 {
		t.Errorf("List returned %d projects, want 1", len(list))
	}

	// Unregister
	if err := repo.Unregister(ctx, "test-proj"); err != nil {
		t.Fatalf("Unregister failed: %v", err)
	}

	// Verify not found
	_, err = repo.Get(ctx, "test-proj")
	if err != domain.ErrProjectNotFound {
		t.Errorf("Get after unregister: got error %v, want %v", err, domain.ErrProjectNotFound)
	}
}

func TestMockCommandExecutor_Execute(t *testing.T) {
	executor := &mockCommandExecutor{}
	ctx := context.Background()

	cmd := &domain.Command{
		ID:        "cmd-1",
		ProjectID: "proj-1",
		Type:      domain.CommandTypeShell,
		Args:      []string{"echo", "hello"},
	}

	var outputLines []domain.OutputLine
	handler := func(line domain.OutputLine) {
		outputLines = append(outputLines, line)
	}

	result, err := executor.Execute(ctx, cmd, "test-pod", handler)
	if err != nil {
		t.Fatalf("Execute failed: %v", err)
	}

	if result.CommandID != "cmd-1" {
		t.Errorf("CommandID = %q, want %q", result.CommandID, "cmd-1")
	}
	if result.ExitCode != 0 {
		t.Errorf("ExitCode = %d, want 0", result.ExitCode)
	}
}

func TestMockAPIKeyRepository_CRUD(t *testing.T) {
	repo := newMockAPIKeyRepository()
	ctx := context.Background()

	key := &domain.APIKey{
		ID:     "key-1",
		Name:   "Test Key",
		Scopes: []domain.Scope{domain.ScopeProjectsRead},
	}

	// Create
	if err := repo.Create(ctx, key, "hash123"); err != nil {
		t.Fatalf("Create failed: %v", err)
	}

	// Get
	got, err := repo.Get(ctx, "key-1")
	if err != nil {
		t.Fatalf("Get failed: %v", err)
	}
	if got.Name != "Test Key" {
		t.Errorf("Name = %q, want %q", got.Name, "Test Key")
	}

	// List
	list, err := repo.List(ctx)
	if err != nil {
		t.Fatalf("List failed: %v", err)
	}
	if len(list) != 1 {
		t.Errorf("List returned %d keys, want 1", len(list))
	}

	// Revoke
	if err := repo.Revoke(ctx, "key-1"); err != nil {
		t.Fatalf("Revoke failed: %v", err)
	}

	// UpdateLastUsed
	if err := repo.UpdateLastUsed(ctx, "key-1"); err != nil {
		t.Fatalf("UpdateLastUsed failed: %v", err)
	}
}

func TestMockStreamPublisher_PubSub(t *testing.T) {
	pub := newMockStreamPublisher()

	// Subscribe
	ch, cleanup := pub.Subscribe("stream-1")
	defer cleanup()

	// Publish
	event := port.StreamEvent{
		ID:   "evt-1",
		Type: "output",
		Data: map[string]any{"line": "hello"},
	}

	eventID := pub.Publish("stream-1", event)
	if eventID != "evt-1" {
		t.Errorf("Publish returned ID %q, want %q", eventID, "evt-1")
	}

	// Receive
	select {
	case received := <-ch:
		if received.ID != "evt-1" {
			t.Errorf("Received event ID %q, want %q", received.ID, "evt-1")
		}
		if received.Data["line"] != "hello" {
			t.Errorf("Received data = %v, want line=hello", received.Data)
		}
	default:
		t.Error("expected to receive event")
	}
}

// =============================================================================
// StreamEvent Tests
// =============================================================================

func TestStreamEvent_CanBeInstantiated(t *testing.T) {
	event := port.StreamEvent{
		ID:   "event-123",
		Type: "command_output",
		Data: map[string]any{
			"stream": "stdout",
			"line":   "test output",
		},
	}

	if event.ID != "event-123" {
		t.Errorf("StreamEvent.ID = %q, want %q", event.ID, "event-123")
	}
	if event.Type != "command_output" {
		t.Errorf("StreamEvent.Type = %q, want %q", event.Type, "command_output")
	}
	if event.Data["stream"] != "stdout" {
		t.Errorf("StreamEvent.Data[stream] = %v, want stdout", event.Data["stream"])
	}
}
