package cmdlimit

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

func TestNew(t *testing.T) {
	t.Run("default config", func(t *testing.T) {
		l := New(Config{})

		if l.cfg.MaxConcurrentPerProject != 5 {
			t.Errorf("MaxConcurrentPerProject = %d, want 5", l.cfg.MaxConcurrentPerProject)
		}
		if l.cfg.MaxConcurrentTotal != 20 {
			t.Errorf("MaxConcurrentTotal = %d, want 20", l.cfg.MaxConcurrentTotal)
		}
		if l.cfg.CommandTimeout != 30*time.Minute {
			t.Errorf("CommandTimeout = %v, want 30m", l.cfg.CommandTimeout)
		}
	})

	t.Run("custom config", func(t *testing.T) {
		l := New(Config{
			MaxConcurrentPerProject: 10,
			MaxConcurrentTotal:      50,
			CommandTimeout:          time.Hour,
		})

		if l.cfg.MaxConcurrentPerProject != 10 {
			t.Errorf("MaxConcurrentPerProject = %d, want 10", l.cfg.MaxConcurrentPerProject)
		}
		if l.cfg.MaxConcurrentTotal != 50 {
			t.Errorf("MaxConcurrentTotal = %d, want 50", l.cfg.MaxConcurrentTotal)
		}
	})
}

func TestDefaultConfig(t *testing.T) {
	cfg := DefaultConfig()

	if cfg.MaxConcurrentPerProject != 5 {
		t.Errorf("MaxConcurrentPerProject = %d, want 5", cfg.MaxConcurrentPerProject)
	}
	if cfg.MaxConcurrentTotal != 20 {
		t.Errorf("MaxConcurrentTotal = %d, want 20", cfg.MaxConcurrentTotal)
	}
	if cfg.CommandTimeout != 30*time.Minute {
		t.Errorf("CommandTimeout = %v, want 30m", cfg.CommandTimeout)
	}
}

func TestAcquire(t *testing.T) {
	t.Run("allows commands within limit", func(t *testing.T) {
		l := New(Config{
			MaxConcurrentPerProject: 3,
			MaxConcurrentTotal:      10,
			CommandTimeout:          time.Minute,
		})
		ctx := context.Background()

		// Should allow 3 commands for project-a
		releases := make([]func(), 0)
		for i := 0; i < 3; i++ {
			release, err := l.Acquire(ctx, "project-a", "cmd-"+string(rune('a'+i)))
			if err != nil {
				t.Fatalf("Acquire %d failed: %v", i, err)
			}
			releases = append(releases, release)
		}

		// Clean up
		for _, r := range releases {
			r()
		}
	})

	t.Run("rejects when per-project limit reached", func(t *testing.T) {
		l := New(Config{
			MaxConcurrentPerProject: 2,
			MaxConcurrentTotal:      10,
			CommandTimeout:          time.Minute,
		})
		ctx := context.Background()

		// Acquire 2 slots
		release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
		defer release1()
		release2, _ := l.Acquire(ctx, "project-a", "cmd-2")
		defer release2()

		// Third should fail
		_, err := l.Acquire(ctx, "project-a", "cmd-3")
		if err != ErrLimitExceeded {
			t.Errorf("Acquire() error = %v, want ErrLimitExceeded", err)
		}
	})

	t.Run("allows other projects when one is at limit", func(t *testing.T) {
		l := New(Config{
			MaxConcurrentPerProject: 1,
			MaxConcurrentTotal:      10,
			CommandTimeout:          time.Minute,
		})
		ctx := context.Background()

		// Fill project-a
		releaseA, _ := l.Acquire(ctx, "project-a", "cmd-a")
		defer releaseA()

		// project-b should still work
		releaseB, err := l.Acquire(ctx, "project-b", "cmd-b")
		if err != nil {
			t.Errorf("Acquire(project-b) error = %v, want nil", err)
		}
		defer releaseB()
	})

	t.Run("rejects when total limit reached", func(t *testing.T) {
		l := New(Config{
			MaxConcurrentPerProject: 5,
			MaxConcurrentTotal:      3,
			CommandTimeout:          time.Minute,
		})
		ctx := context.Background()

		// Fill total limit across different projects
		release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
		defer release1()
		release2, _ := l.Acquire(ctx, "project-b", "cmd-2")
		defer release2()
		release3, _ := l.Acquire(ctx, "project-c", "cmd-3")
		defer release3()

		// Fourth should fail even for new project
		_, err := l.Acquire(ctx, "project-d", "cmd-4")
		if err != ErrLimitExceeded {
			t.Errorf("Acquire() error = %v, want ErrLimitExceeded", err)
		}
	})

	t.Run("release allows new commands", func(t *testing.T) {
		l := New(Config{
			MaxConcurrentPerProject: 1,
			MaxConcurrentTotal:      10,
			CommandTimeout:          time.Minute,
		})
		ctx := context.Background()

		// Acquire and release
		release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
		release1()

		// Should be able to acquire again
		release2, err := l.Acquire(ctx, "project-a", "cmd-2")
		if err != nil {
			t.Errorf("Acquire() after release error = %v, want nil", err)
		}
		release2()
	})
}

func TestStats(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 5,
		MaxConcurrentTotal:      20,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
	release2, _ := l.Acquire(ctx, "project-a", "cmd-2")
	release3, _ := l.Acquire(ctx, "project-b", "cmd-3")

	stats := l.Stats()

	if stats.TotalActive != 3 {
		t.Errorf("TotalActive = %d, want 3", stats.TotalActive)
	}
	if stats.MaxTotal != 20 {
		t.Errorf("MaxTotal = %d, want 20", stats.MaxTotal)
	}
	if stats.ProjectCounts["project-a"] != 2 {
		t.Errorf("ProjectCounts[project-a] = %d, want 2", stats.ProjectCounts["project-a"])
	}
	if stats.ProjectCounts["project-b"] != 1 {
		t.Errorf("ProjectCounts[project-b] = %d, want 1", stats.ProjectCounts["project-b"])
	}
	if len(stats.ActiveCommandIDs) != 3 {
		t.Errorf("ActiveCommandIDs length = %d, want 3", len(stats.ActiveCommandIDs))
	}

	release1()
	release2()
	release3()
}

func TestIsProjectAtLimit(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 2,
		MaxConcurrentTotal:      10,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	if l.IsProjectAtLimit("project-a") {
		t.Error("IsProjectAtLimit should be false initially")
	}

	release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
	release2, _ := l.Acquire(ctx, "project-a", "cmd-2")

	if !l.IsProjectAtLimit("project-a") {
		t.Error("IsProjectAtLimit should be true after reaching limit")
	}

	release1()
	release2()
}

func TestIsTotalAtLimit(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 5,
		MaxConcurrentTotal:      2,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	if l.IsTotalAtLimit() {
		t.Error("IsTotalAtLimit should be false initially")
	}

	release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
	release2, _ := l.Acquire(ctx, "project-b", "cmd-2")

	if !l.IsTotalAtLimit() {
		t.Error("IsTotalAtLimit should be true after reaching limit")
	}

	release1()
	release2()
}

func TestActiveCount(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 5,
		MaxConcurrentTotal:      10,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	if l.ActiveCount("project-a") != 0 {
		t.Error("ActiveCount should be 0 initially")
	}

	release1, _ := l.Acquire(ctx, "project-a", "cmd-1")

	if l.ActiveCount("project-a") != 1 {
		t.Errorf("ActiveCount = %d, want 1", l.ActiveCount("project-a"))
	}

	release1()

	if l.ActiveCount("project-a") != 0 {
		t.Errorf("ActiveCount after release = %d, want 0", l.ActiveCount("project-a"))
	}
}

func TestTotalActiveCount(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 5,
		MaxConcurrentTotal:      10,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	if l.TotalActiveCount() != 0 {
		t.Error("TotalActiveCount should be 0 initially")
	}

	release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
	release2, _ := l.Acquire(ctx, "project-b", "cmd-2")

	if l.TotalActiveCount() != 2 {
		t.Errorf("TotalActiveCount = %d, want 2", l.TotalActiveCount())
	}

	release1()
	release2()

	if l.TotalActiveCount() != 0 {
		t.Errorf("TotalActiveCount after release = %d, want 0", l.TotalActiveCount())
	}
}

func TestConcurrentAccess(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 10,
		MaxConcurrentTotal:      100,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	var wg sync.WaitGroup
	var successCount, failCount int64
	var mu sync.Mutex

	// Spawn many goroutines trying to acquire
	for i := 0; i < 150; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()

			release, err := l.Acquire(ctx, "project-a", "cmd-"+string(rune(idx)))
			mu.Lock()
			if err == nil {
				successCount++
				mu.Unlock()
				time.Sleep(10 * time.Millisecond)
				release()
			} else {
				failCount++
				mu.Unlock()
			}
		}(i)
	}

	wg.Wait()

	// Should have had some successes and some failures
	if successCount == 0 {
		t.Error("Expected some successful acquires")
	}
	if failCount == 0 {
		t.Error("Expected some failed acquires (limit exceeded)")
	}
}

func TestAutoReleaseOnTimeout(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 1,
		MaxConcurrentTotal:      10,
		CommandTimeout:          100 * time.Millisecond,
	})
	ctx := context.Background()

	// Acquire a slot
	_, err := l.Acquire(ctx, "project-a", "cmd-1")
	if err != nil {
		t.Fatalf("Acquire failed: %v", err)
	}

	// Should be at limit
	if !l.IsProjectAtLimit("project-a") {
		t.Error("Should be at limit after acquire")
	}

	// Wait for timeout
	time.Sleep(150 * time.Millisecond)

	// Should be auto-released
	if l.IsProjectAtLimit("project-a") {
		t.Error("Should not be at limit after auto-release")
	}
}

func TestDoubleRelease(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 5,
		MaxConcurrentTotal:      10,
		CommandTimeout:          time.Minute,
	})
	ctx := context.Background()

	release, _ := l.Acquire(ctx, "project-a", "cmd-1")

	// Release twice - should not panic or cause issues
	release()
	release()

	// Count should be 0, not negative
	if l.ActiveCount("project-a") != 0 {
		t.Errorf("ActiveCount after double release = %d, want 0", l.ActiveCount("project-a"))
	}
}

func TestContextCancellation(t *testing.T) {
	l := New(Config{
		MaxConcurrentPerProject: 1,
		MaxConcurrentTotal:      10,
		CommandTimeout:          time.Minute,
	})

	ctx, cancel := context.WithCancel(context.Background())

	// Acquire a slot
	_, err := l.Acquire(ctx, "project-a", "cmd-1")
	if err != nil {
		t.Fatalf("Acquire failed: %v", err)
	}

	// Cancel the context
	cancel()

	// Give goroutine time to process
	time.Sleep(50 * time.Millisecond)

	// Should be auto-released due to context cancellation
	if l.IsProjectAtLimit("project-a") {
		t.Error("Should be released after context cancellation")
	}
}
