package cli

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

// interceptStateStub stands in for the platform pfState/wfpState; the
// recovery cleanup path only checks dnsInterceptState != nil.
type interceptStateStub struct{}

// setupInterceptRecovery puts p into "intercept-mode recovery in flight"
// state and restores the package-level dnsIntercept flag on cleanup.
func setupInterceptRecovery(t *testing.T, p *prog) {
	t.Helper()
	oldIntercept := dnsIntercept
	dnsIntercept = true
	t.Cleanup(func() { dnsIntercept = oldIntercept })
	p.dnsInterceptState = &interceptStateStub{}
	p.logger.Store(mainLog.Load())
	p.recoveryBypass.Store(true)
	p.recoveryRunning.Store(true)
	p.recoveryCancel = func() {}
}

// TestRecoveryCanceledCleanup_LastRecoveryResetsState pins issue #597: a
// canceled recovery with no successor must clear recoveryBypass and
// recoveryRunning, or the daemon stays in bypass forever (every query
// detours to the OS resolver) and the DNS watchdog stays disabled.
func TestRecoveryCanceledCleanup_LastRecoveryResetsState(t *testing.T) {
	p := &prog{}
	setupInterceptRecovery(t, p)
	gen := p.recoveryGen.Add(1)

	p.recoveryCanceledCleanup(gen)

	if p.recoveryBypass.Load() {
		t.Error("recoveryBypass still set after canceled recovery with no successor")
	}
	if p.recoveryRunning.Load() {
		t.Error("recoveryRunning still set after canceled recovery with no successor")
	}
	p.recoveryCancelMu.Lock()
	cancelCleared := p.recoveryCancel == nil
	p.recoveryCancelMu.Unlock()
	if !cancelCleared {
		t.Error("recoveryCancel not cleared after canceled recovery with no successor")
	}
}

// TestRecoveryCanceledCleanup_SupersededKeepsSuccessorState pins the
// captive-portal/network-flap contract: when a newer recovery superseded the
// canceled one, the canceled recovery must NOT clear shared state — the
// successor owns bypass for its own duration.
func TestRecoveryCanceledCleanup_SupersededKeepsSuccessorState(t *testing.T) {
	p := &prog{}
	setupInterceptRecovery(t, p)
	gen := p.recoveryGen.Add(1)
	// A successor recovery started.
	p.recoveryGen.Add(1)

	p.recoveryCanceledCleanup(gen)

	if !p.recoveryBypass.Load() {
		t.Error("superseded canceled recovery cleared recoveryBypass owned by its successor")
	}
	if !p.recoveryRunning.Load() {
		t.Error("superseded canceled recovery cleared recoveryRunning owned by its successor")
	}
	p.recoveryCancelMu.Lock()
	cancelKept := p.recoveryCancel != nil
	p.recoveryCancelMu.Unlock()
	if !cancelKept {
		t.Error("superseded canceled recovery cleared the successor's recoveryCancel")
	}
}

// TestRecoveryCanceledCleanup_NonInterceptResetsRunning covers traditional
// (non-intercept) mode: recoveryRunning must still be reset so watchdogs
// resume, while bypass is untouched (it is never set in that mode).
func TestRecoveryCanceledCleanup_NonInterceptResetsRunning(t *testing.T) {
	oldIntercept := dnsIntercept
	dnsIntercept = false
	t.Cleanup(func() { dnsIntercept = oldIntercept })

	p := &prog{}
	p.logger.Store(mainLog.Load())
	p.recoveryRunning.Store(true)
	p.recoveryCancel = func() {}
	gen := p.recoveryGen.Add(1)

	p.recoveryCanceledCleanup(gen)

	if p.recoveryRunning.Load() {
		t.Error("recoveryRunning still set after canceled non-intercept recovery")
	}
}

func TestBeginRecoveryTransfersOwnershipAtomically(t *testing.T) {
	oldIntercept := dnsIntercept
	dnsIntercept = true
	t.Cleanup(func() { dnsIntercept = oldIntercept })

	p := &prog{dnsInterceptState: &interceptStateStub{}}
	firstCtx, firstGen, _, ok := p.beginRecovery(RecoveryReasonRegularFailure)
	if !ok {
		t.Fatal("first recovery did not acquire ownership")
	}
	if _, _, _, ok := p.beginRecovery(RecoveryReasonRegularFailure); ok {
		t.Fatal("duplicate upstream recovery acquired ownership")
	}

	_, successorGen, intercept, ok := p.beginRecovery(RecoveryReasonNetworkChange)
	if !ok || !intercept || successorGen <= firstGen {
		t.Fatalf("network recovery did not replace owner: first=%d successor=%d intercept=%v ok=%v", firstGen, successorGen, intercept, ok)
	}
	select {
	case <-firstCtx.Done():
	case <-time.After(time.Second):
		t.Fatal("successor did not cancel the previous recovery")
	}

	p.recoveryCanceledCleanup(firstGen)
	if !p.recoveryRunning.Load() || !p.recoveryBypass.Load() || !p.recoveryOwnsState(successorGen) {
		t.Fatal("stale cleanup changed successor-owned recovery state")
	}
	if !p.completeRecoveryState(successorGen) {
		t.Fatal("successor could not complete its own recovery state")
	}
}

func TestRecoveryCleanupClearsBypassAfterInterceptStateDisappears(t *testing.T) {
	p := &prog{}
	p.logger.Store(mainLog.Load())
	p.recoveryBypass.Store(true)
	p.recoveryRunning.Store(true)
	p.recoveryCancel = func() {}
	gen := p.recoveryGen.Add(1)

	p.recoveryCanceledCleanup(gen)
	if p.recoveryBypass.Load() || p.recoveryRunning.Load() {
		t.Fatal("cleanup retained recovery flags after intercept state disappeared")
	}
}

func TestSystemNameserversForInterceptRetryNormalizesEmptyDiscovery(t *testing.T) {
	original := initializeOsResolverWithSystemNameserversFn
	called := false
	initializeOsResolverWithSystemNameserversFn = func(_ context.Context, guard bool) ([]string, []string) {
		called = true
		if !guard {
			t.Error("intercept retry discovery did not guard the existing resolver")
		}
		return nil, nil
	}
	t.Cleanup(func() { initializeOsResolverWithSystemNameserversFn = original })

	p := &prog{}
	p.logger.Store(mainLog.Load())
	if got := p.systemNameserversForInterceptRetry(); got == nil || len(got) != 0 {
		t.Fatalf("system discovery = %#v, want non-nil empty slice", got)
	}
	if !called {
		t.Fatal("system discovery was not called")
	}
}
