package daemon

import (
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
)

// This file holds deterministic tests for the machineObservationGate read/write
// gate. They exercise the zero value directly (the gate's concurrency-safe
// one-time init), the authoritative waiting/acquired stage semantics, writer
// non-starvation, and release/acquire invariant preservation — all without any
// fixed sleep as contention evidence (stage channels + completion signals are
// the only synchronization).

// TestMachineObservationGateConcurrentZeroValueFirstUse proves the zero-value
// gate is safe for concurrent first use: many readers/writers start
// simultaneously on a freshly-zeroed gate, all complete, and the gate still
// behaves correctly afterward. Run under -race to catch the init() data race.
func TestMachineObservationGateConcurrentZeroValueFirstUse(t *testing.T) {
	var gate machineObservationGate
	const (
		readers = 8
		writers = 4
	)
	var wg sync.WaitGroup
	// Start all goroutines simultaneously: barrier ensures first use is truly
	// concurrent (no goroutine wins init() before the others start).
	start := make(chan struct{})
	for i := 0; i < readers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			gate.acquireRead()
			runtime.Gosched()
			gate.releaseRead()
		}()
	}
	for i := 0; i < writers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			gate.acquireWrite(nil, nil)
			runtime.Gosched()
			gate.releaseWrite()
		}()
	}
	close(start)
	wg.Wait()

	// The gate must still be fully usable after the concurrent first use.
	gate.acquireRead()
	if gate.readers != 1 {
		t.Fatalf("after concurrent first use, reader count = %d, want 1", gate.readers)
	}
	gate.releaseRead()
}

// TestMachineObservationGateUncontendedWriterAcquiredNoWaiting proves an
// uncontended writer fires acquired but NEVER waiting (a writer that is not
// blocked by an active reader/writer must not register as waiting).
func TestMachineObservationGateUncontendedWriterAcquiredNoWaiting(t *testing.T) {
	var gate machineObservationGate
	var waiting atomic.Bool
	var acquired atomic.Bool
	gate.acquireWrite(func() { waiting.Store(true) }, func() { acquired.Store(true) })
	if acquired.Load() != true {
		t.Fatal("uncontended writer must fire writeAcquired")
	}
	if waiting.Load() != false {
		t.Fatal("uncontended writer must NOT fire writeWaiting (no active reader/writer)")
	}
	gate.releaseWrite()
}

// TestMachineObservationGateReaderHeldWriterWaitsThenCompletes proves a writer
// blocked by an active reader fires waiting BEFORE acquired, and completes only
// after the reader releases.
func TestMachineObservationGateReaderHeldWriterWaitsThenCompletes(t *testing.T) {
	var gate machineObservationGate

	gate.acquireRead() // reader holds the read side

	var waitingCh = make(chan struct{})
	var acquiredCh = make(chan struct{})
	var doneCh = make(chan struct{})
	go func() {
		gate.acquireWrite(
			func() { close(waitingCh) },
			func() { close(acquiredCh) },
		)
		close(doneCh)
	}()

	// The writer must register as waiting (blocked by the active reader) before
	// it can acquire.
	<-waitingCh
	select {
	case <-acquiredCh:
		t.Fatal("writer acquired while a reader still holds the read side")
	default:
	}
	select {
	case <-doneCh:
		t.Fatal("writer completed while a reader still holds the read side")
	default:
	}

	// Releasing the reader unblocks the writer: waiting fires before acquired,
	// and both complete after the release.
	gate.releaseRead()
	<-acquiredCh
	<-doneCh
}

// TestMachineObservationGateNewReadersQueueBehindRegisteredWriter proves a newly
// arriving reader must not bypass a registered (waiting) writer, so the writer
// progresses (no starvation).
func TestMachineObservationGateNewReadersQueueBehindRegisteredWriter(t *testing.T) {
	var gate machineObservationGate

	gate.acquireRead() // reader A holds the read side

	var waitingCh = make(chan struct{})
	var writerDone = make(chan struct{})
	go func() {
		gate.acquireWrite(func() { close(waitingCh) }, nil)
		close(writerDone)
	}()

	// Writer registers as waiting (blocked by reader A).
	<-waitingCh

	// A new reader arriving now must queue behind the registered writer.
	var readerAcquired = make(chan struct{})
	var readerDone = make(chan struct{})
	go func() {
		gate.acquireRead()
		close(readerAcquired)
		close(readerDone)
	}()

	// The new reader must NOT acquire while the writer is still waiting.
	select {
	case <-readerAcquired:
		t.Fatal("new reader bypassed the registered writer (starvation)")
	default:
	}
	select {
	case <-writerDone:
		t.Fatal("writer completed while reader A still holds the read side")
	default:
	}

	// Release reader A: the registered writer proceeds before the queued reader,
	// acquires the write, and then releases it so the queued reader can proceed.
	gate.releaseRead()
	<-writerDone
	gate.releaseWrite()
	<-readerAcquired
	<-readerDone
}

// TestMachineObservationGateRepeatedReleaseAcquirePreservesInvariants proves
// many release/acquire cycles (interleaved readers and writers) keep the gate's
// invariants intact: readers/writers never deadlock and the gate remains
// correct.
func TestMachineObservationGateRepeatedReleaseAcquirePreservesInvariants(t *testing.T) {
	var gate machineObservationGate
	const cycles = 50
	var wg sync.WaitGroup
	for i := 0; i < cycles; i++ {
		wg.Add(2)
		go func() {
			defer wg.Done()
			gate.acquireRead()
			runtime.Gosched()
			gate.releaseRead()
		}()
		go func() {
			defer wg.Done()
			gate.acquireWrite(nil, nil)
			runtime.Gosched()
			gate.releaseWrite()
		}()
	}
	wg.Wait()

	// Invariant: no reader may be active while a writer owns the write, and the
	// gate must be fully released afterward.
	gate.mu.Lock()
	if gate.writer != false {
		t.Fatal("gate writer must be released after all cycles")
	}
	if gate.readers != 0 {
		t.Fatalf("gate readers must be zero after all cycles, got %d", gate.readers)
	}
	gate.mu.Unlock()

	// A final uncontended writer still behaves correctly (no waiting).
	var waiting atomic.Bool
	var acquired atomic.Bool
	gate.acquireWrite(func() { waiting.Store(true) }, func() { acquired.Store(true) })
	if waiting.Load() != false {
		t.Fatal("final uncontended writer must not fire waiting")
	}
	if acquired.Load() != true {
		t.Fatal("final uncontended writer must fire acquired")
	}
	gate.releaseWrite()
}

// TestMachineObservationGateAtomicCheckClaimNoContentionGap proves the
// check-to-claim is atomic under one gate.mu hold: a writer that observes the
// gate free must claim ownership (writer = true) before the first unlock — a
// reader/writer can never slip in between the check and the claim, which would
// leave the writer silently waiting without a waitingWriters registration.
//
// Algorithm-invariant assertion: with no reader and no owning writer, the writer
// acquires and the gate immediately reports writer-owned (readers==0 && writer)
// with waitingWriters==0, and NO waiting stage fired. Any implementation that
// checked-free then unlocked before claiming would still acquire, but the
// gate's waitingWriters would remain 0 while the writer was blocked in the
// (previously separate) claim relock — a reader could have been admitted in
// that window. This test also stresses the atomic path under heavy concurrent
// reader/writer churn to prove the writer-preference invariant still holds
// (every writer either claims immediately on a free gate or registers as
// waiting; no writer is silently bypassed).
func TestMachineObservationGateAtomicCheckClaimNoContentionGap(t *testing.T) {
	var gate machineObservationGate

	// Single uncontended atomic claim: the writer must fire acquired with NO
	// waiting stage, and immediately own the write.
	var waiting atomic.Bool
	var acquired atomic.Bool
	gate.acquireWrite(func() { waiting.Store(true) }, func() { acquired.Store(true) })
	if acquired.Load() != true {
		t.Fatal("free-gate writer must fire acquired")
	}
	if waiting.Load() != false {
		t.Fatal("free-gate writer must not fire waiting (atomic claim, no block)")
	}
	gate.mu.Lock()
	if !gate.writer {
		t.Fatal("free-gate writer must own the write immediately after acquire")
	}
	if gate.readers != 0 {
		t.Fatalf("free-gate writer must hold zero readers, got %d", gate.readers)
	}
	if gate.waitingWriters != 0 {
		t.Fatalf("free-gate writer must register zero waiting writers, got %d", gate.waitingWriters)
	}
	gate.mu.Unlock()
	gate.releaseWrite()

	// Stress: concurrent readers/writers churn. Every writer must either claim
	// the write immediately (free at check -> atomic claim) or register as
	// waiting (blocked). No writer may silently wait without a registration.
	// We cannot observe a specific writer's blocked state here without a seam,
	// so assert the writer-preference invariant after churn: after every cycle
	// the gate returns to a fully-released state, and no writer ever acquired
	// while a reader held the read side (a writer that fired acquired must have
	// owned the write exclusively).
	const cycles = 60
	var wg sync.WaitGroup
	var mutualExclusionViolations atomic.Int64
	for i := 0; i < cycles; i++ {
		wg.Add(3)
		// Reader that holds the read side briefly.
		go func() {
			defer wg.Done()
			gate.acquireRead()
			runtime.Gosched()
			gate.releaseRead()
		}()
		// Writer A.
		go func() {
			defer wg.Done()
			gate.acquireWrite(nil, func() {
				// While holding the write, no reader may be active.
				gate.mu.Lock()
				if gate.readers != 0 {
					mutualExclusionViolations.Add(1)
				}
				gate.mu.Unlock()
			})
			gate.releaseWrite()
		}()
		// Writer B (contends with A for the write side).
		go func() {
			defer wg.Done()
			gate.acquireWrite(nil, nil)
			gate.releaseWrite()
		}()
	}
	wg.Wait()
	if mutualExclusionViolations.Load() != 0 {
		t.Fatalf("writer acquired while a reader was active %d time(s)", mutualExclusionViolations.Load())
	}
	gate.mu.Lock()
	defer gate.mu.Unlock()
	if gate.writer || gate.readers != 0 || gate.waitingWriters != 0 {
		t.Fatalf("gate must be fully released after churn: writer=%v readers=%d waiting=%d",
			gate.writer, gate.readers, gate.waitingWriters)
	}
}
