package reconcile

import (
	"errors"
	"sync"
	"testing"
	"time"
)

// TestQueue_SameGuestSerialized asserts that jobs for one vmid run strictly
// one-at-a-time in submit order — the core §10 guarantee that keeps Proxmox from
// seeing concurrent conflicting ops on a guest.
func TestQueue_SameGuestSerialized(t *testing.T) {
	q := NewQueue()
	defer q.Close()

	const n = 50
	var mu sync.Mutex
	var order []int
	inside := 0
	maxConcurrent := 0

	chans := make([]<-chan error, n)
	for i := 0; i < n; i++ {
		i := i
		chans[i] = q.Submit(100, func() error {
			mu.Lock()
			inside++
			if inside > maxConcurrent {
				maxConcurrent = inside
			}
			order = append(order, i)
			mu.Unlock()

			time.Sleep(time.Millisecond) // widen any overlap window

			mu.Lock()
			inside--
			mu.Unlock()
			return nil
		})
	}
	for _, ch := range chans {
		if err := <-ch; err != nil {
			t.Fatalf("unexpected job error: %v", err)
		}
	}

	if maxConcurrent != 1 {
		t.Errorf("same-guest jobs overlapped: maxConcurrent=%d, want 1", maxConcurrent)
	}
	for i := 0; i < n; i++ {
		if order[i] != i {
			t.Fatalf("same-guest jobs ran out of submit order: got %v", order)
			break
		}
	}
}

// TestQueue_DifferentGuestsParallel asserts independent vmids proceed concurrently:
// two jobs on different lanes that each wait for the other before finishing must BOTH
// complete (they'd deadlock under a global lock / single worker).
func TestQueue_DifferentGuestsParallel(t *testing.T) {
	q := NewQueue()
	defer q.Close()

	aReady := make(chan struct{})
	bReady := make(chan struct{})

	chA := q.Submit(1, func() error {
		close(aReady)
		select {
		case <-bReady:
			return nil
		case <-time.After(2 * time.Second):
			return errors.New("guest 1 timed out waiting for guest 2 (not parallel)")
		}
	})
	chB := q.Submit(2, func() error {
		close(bReady)
		select {
		case <-aReady:
			return nil
		case <-time.After(2 * time.Second):
			return errors.New("guest 2 timed out waiting for guest 1 (not parallel)")
		}
	})

	if err := <-chA; err != nil {
		t.Error(err)
	}
	if err := <-chB; err != nil {
		t.Error(err)
	}
}

// TestQueue_PropagatesJobError confirms a job's error reaches its result channel.
func TestQueue_PropagatesJobError(t *testing.T) {
	q := NewQueue()
	defer q.Close()
	want := errors.New("boom")
	if got := <-q.Submit(7, func() error { return want }); got != want {
		t.Errorf("Submit result = %v, want %v", got, want)
	}
}

// TestQueue_DrainsPendingOnClose confirms jobs already queued before Close still run.
func TestQueue_DrainsPendingOnClose(t *testing.T) {
	q := NewQueue()
	release := make(chan struct{})
	var ran sync.WaitGroup
	ran.Add(2)

	// First job blocks until released, pinning the lane so the second sits pending.
	ch1 := q.Submit(5, func() error { <-release; ran.Done(); return nil })
	ch2 := q.Submit(5, func() error { ran.Done(); return nil })

	q.Close() // close while job1 is queued/running and job2 is pending
	close(release)

	if err := <-ch1; err != nil {
		t.Errorf("job1 err: %v", err)
	}
	if err := <-ch2; err != nil {
		t.Errorf("pending job2 should still run after Close, got: %v", err)
	}
	ran.Wait()
}

// TestQueue_SubmitAfterClose returns ErrQueueClosed.
func TestQueue_SubmitAfterClose(t *testing.T) {
	q := NewQueue()
	q.Close()
	if got := <-q.Submit(1, func() error { return nil }); got != ErrQueueClosed {
		t.Errorf("Submit after Close = %v, want ErrQueueClosed", got)
	}
}
