package cached

import (
	"context"
	"sync"
	"testing"
	"time"

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

// mockProjectRepository is a test double for port.ProjectRepository
type mockProjectRepository struct {
	projects     []domain.Project
	listCalls    int
	refreshCalls int
	mu           sync.Mutex
}

func (m *mockProjectRepository) List(ctx context.Context) ([]domain.Project, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.listCalls++
	return m.projects, nil
}

func (m *mockProjectRepository) Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	for i := range m.projects {
		if m.projects[i].ID == id {
			return &m.projects[i], nil
		}
	}
	return nil, domain.ErrProjectNotFound
}

func (m *mockProjectRepository) Exists(ctx context.Context, id domain.ProjectID) (bool, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	for _, p := range m.projects {
		if p.ID == id {
			return true, nil
		}
	}
	return false, nil
}

func (m *mockProjectRepository) RefreshStatus(ctx context.Context) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.refreshCalls++
	return nil
}

func (m *mockProjectRepository) Register(ctx context.Context, p *domain.Project) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.projects = append(m.projects, *p)
	return nil
}

func (m *mockProjectRepository) Unregister(ctx context.Context, id domain.ProjectID) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	for i, p := range m.projects {
		if p.ID == id {
			m.projects = append(m.projects[:i], m.projects[i+1:]...)
			break
		}
	}
	return nil
}

func TestCachedProjectRepository_List_Caches(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
			{ID: "proj-2", Name: "Project 2"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	// First call should hit inner repository
	projects1, err := repo.List(ctx)
	if err != nil {
		t.Fatalf("List() error = %v", err)
	}
	if len(projects1) != 2 {
		t.Errorf("List() returned %d projects, want 2", len(projects1))
	}
	if mock.listCalls != 1 {
		t.Errorf("Inner List called %d times, want 1", mock.listCalls)
	}

	// Second call should use cache
	projects2, err := repo.List(ctx)
	if err != nil {
		t.Fatalf("List() error = %v", err)
	}
	if len(projects2) != 2 {
		t.Errorf("List() returned %d projects, want 2", len(projects2))
	}
	if mock.listCalls != 1 {
		t.Errorf("Inner List should not be called again, was called %d times", mock.listCalls)
	}
}

func TestCachedProjectRepository_List_Expires(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	// Very short TTL for testing
	repo := NewProjectRepository(mock, 50*time.Millisecond)
	ctx := context.Background()

	// First call
	_, _ = repo.List(ctx)
	if mock.listCalls != 1 {
		t.Errorf("Expected 1 call, got %d", mock.listCalls)
	}

	// Wait for cache to expire
	time.Sleep(60 * time.Millisecond)

	// Should hit inner repository again
	_, _ = repo.List(ctx)
	if mock.listCalls != 2 {
		t.Errorf("Expected 2 calls after expiry, got %d", mock.listCalls)
	}
}

func TestCachedProjectRepository_Get_UsesCache(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	// Warm the cache
	_, _ = repo.List(ctx)

	// Get should use cached data
	project, err := repo.Get(ctx, "proj-1")
	if err != nil {
		t.Fatalf("Get() error = %v", err)
	}
	if project.Name != "Project 1" {
		t.Errorf("Name = %q, want %q", project.Name, "Project 1")
	}

	// Should not have called List again
	if mock.listCalls != 1 {
		t.Errorf("Inner List called %d times, want 1", mock.listCalls)
	}
}

func TestCachedProjectRepository_Get_NotFound(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	_, err := repo.Get(ctx, "nonexistent")
	if err != domain.ErrProjectNotFound {
		t.Errorf("Get(nonexistent) error = %v, want ErrProjectNotFound", err)
	}
}

func TestCachedProjectRepository_Exists(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	exists, err := repo.Exists(ctx, "proj-1")
	if err != nil {
		t.Fatalf("Exists() error = %v", err)
	}
	if !exists {
		t.Error("Exists(proj-1) = false, want true")
	}

	exists, err = repo.Exists(ctx, "nonexistent")
	if err != nil {
		t.Fatalf("Exists() error = %v", err)
	}
	if exists {
		t.Error("Exists(nonexistent) = true, want false")
	}
}

func TestCachedProjectRepository_RefreshStatus_InvalidatesCache(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	// Warm cache
	_, _ = repo.List(ctx)
	if mock.listCalls != 1 {
		t.Errorf("Expected 1 call, got %d", mock.listCalls)
	}

	// Refresh status should invalidate cache
	_ = repo.RefreshStatus(ctx)

	// Next List should hit inner repository
	_, _ = repo.List(ctx)
	if mock.listCalls != 2 {
		t.Errorf("Expected 2 calls after RefreshStatus, got %d", mock.listCalls)
	}
}

func TestCachedProjectRepository_Register_InvalidatesCache(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	// Warm cache
	_, _ = repo.List(ctx)

	// Register should invalidate cache
	_ = repo.Register(ctx, &domain.Project{ID: "new-proj", Name: "New"})

	// Next List should hit inner repository
	_, _ = repo.List(ctx)
	if mock.listCalls != 2 {
		t.Errorf("Expected 2 calls after Register, got %d", mock.listCalls)
	}
}

func TestCachedProjectRepository_Invalidate(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	// Warm cache
	_, _ = repo.List(ctx)

	// Manually invalidate
	repo.Invalidate()

	// Next List should hit inner repository
	_, _ = repo.List(ctx)
	if mock.listCalls != 2 {
		t.Errorf("Expected 2 calls after Invalidate, got %d", mock.listCalls)
	}
}

func TestCachedProjectRepository_CacheStats(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
			{ID: "proj-2", Name: "Project 2"},
		},
	}

	repo := NewProjectRepository(mock, 1*time.Minute)
	ctx := context.Background()

	// Before warming
	stats := repo.CacheStats()
	if stats.IsFresh {
		t.Error("Cache should not be fresh before List")
	}
	if stats.Size != 0 {
		t.Errorf("Size = %d, want 0", stats.Size)
	}

	// After warming
	_, _ = repo.List(ctx)
	stats = repo.CacheStats()
	if !stats.IsFresh {
		t.Error("Cache should be fresh after List")
	}
	if stats.Size != 2 {
		t.Errorf("Size = %d, want 2", stats.Size)
	}
	if stats.TTL != 1*time.Minute {
		t.Errorf("TTL = %v, want 1m", stats.TTL)
	}
}

func TestCachedProjectRepository_Concurrent(t *testing.T) {
	mock := &mockProjectRepository{
		projects: []domain.Project{
			{ID: "proj-1", Name: "Project 1"},
		},
	}

	repo := NewProjectRepository(mock, 50*time.Millisecond)
	ctx := context.Background()

	var wg sync.WaitGroup

	// Concurrent List calls
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			repo.List(ctx)
		}()
	}

	// Concurrent Get calls
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, _ = repo.Get(ctx, "proj-1")
		}()
	}

	// Concurrent Exists calls
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, _ = repo.Exists(ctx, "proj-1")
		}()
	}

	wg.Wait()
	// Test passes if no race/deadlock
}

func TestNewProjectRepository_DefaultTTL(t *testing.T) {
	mock := &mockProjectRepository{}
	repo := NewProjectRepository(mock, 0) // Zero TTL should use default

	stats := repo.CacheStats()
	if stats.TTL != 30*time.Second {
		t.Errorf("TTL = %v, want 30s (default)", stats.TTL)
	}
}
