package runtimeproduction

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus/testutil"
)

func TestServingControllerRetriesAuthorityFailureAndReconcilesRecoveredEpoch(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	var refreshes atomic.Int64
	reconciled := make(chan uint64, 1)
	done := make(chan error, 1)
	go func() {
		done <- runServingController(ctx, 10*time.Millisecond, 5*time.Millisecond, 6,
			func(context.Context) error {
				if refreshes.Add(1) == 1 {
					return errors.New("authority unavailable")
				}
				return nil
			},
			func() (uint64, bool) { return 7, true },
			func(_ context.Context, epoch uint64) error { reconciled <- epoch; return nil },
			nil,
		)
	}()
	select {
	case epoch := <-reconciled:
		if epoch != 7 || refreshes.Load() < 2 {
			t.Fatalf("epoch=%d refreshes=%d", epoch, refreshes.Load())
		}
	case <-time.After(time.Second):
		t.Fatal("controller did not recover after an authority failure")
	}
	cancel()
	if err := <-done; err != nil {
		t.Fatal(err)
	}
}

func TestServingControllerRefreshesBeforeFirstIntervalWithoutReplayingCurrentEpoch(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	refreshed := make(chan struct{}, 1)
	reconciled := make(chan uint64, 1)
	done := make(chan error, 1)
	go func() {
		done <- runServingController(ctx, time.Hour, time.Second, 9,
			func(context.Context) error { refreshed <- struct{}{}; return nil },
			func() (uint64, bool) { return 9, true },
			func(_ context.Context, epoch uint64) error { reconciled <- epoch; return nil },
			nil,
		)
	}()
	select {
	case <-refreshed:
	case <-time.After(time.Second):
		t.Fatal("controller waited for its first interval before refreshing authority")
	}
	select {
	case epoch := <-reconciled:
		t.Fatalf("controller replayed already reconciled epoch %d", epoch)
	case <-time.After(20 * time.Millisecond):
	}
	cancel()
	if err := <-done; err != nil {
		t.Fatal(err)
	}
}

func TestServingControllerRenewsWhileEpochReconcileIsBlocked(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	var refreshes atomic.Int64
	reconcileStarted := make(chan struct{})
	releaseReconcile := make(chan struct{})
	done := make(chan error, 1)
	go func() {
		done <- runServingController(
			ctx, 2*time.Millisecond, time.Millisecond, 4,
			func(context.Context) error { refreshes.Add(1); return nil },
			func() (uint64, bool) { return 5, true },
			func(ctx context.Context, _ uint64) error {
				close(reconcileStarted)
				select {
				case <-releaseReconcile:
					return nil
				case <-ctx.Done():
					return ctx.Err()
				}
			},
			nil,
		)
	}()
	select {
	case <-reconcileStarted:
	case <-time.After(time.Second):
		t.Fatal("epoch reconcile did not start")
	}
	deadline := time.After(time.Second)
	for refreshes.Load() < 3 {
		select {
		case <-deadline:
			t.Fatalf("lease renewal stopped behind reconcile; refreshes=%d", refreshes.Load())
		case <-time.After(time.Millisecond):
		}
	}
	close(releaseReconcile)
	cancel()
	if err := <-done; err != nil {
		t.Fatal(err)
	}
}

func TestServingControllerBoundsHungRenewalAndReportsRecovery(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	metrics := newServingControllerMetrics()
	var attempts atomic.Int64
	recovered := make(chan struct{}, 1)
	done := make(chan error, 1)
	go func() {
		done <- runServingController(
			ctx, 4*time.Millisecond, time.Millisecond, 3,
			func(ctx context.Context) error {
				if attempts.Add(1) == 1 {
					<-ctx.Done()
					return ctx.Err()
				}
				recovered <- struct{}{}
				return nil
			},
			func() (uint64, bool) { return 3, true },
			func(context.Context, uint64) error { return nil },
			metrics,
		)
	}()
	select {
	case <-recovered:
	case <-time.After(time.Second):
		t.Fatal("controller did not retry after bounded renewal timeout")
	}
	deadline := time.After(time.Second)
	for metrics.consecutiveFailures.Load() != 0 || metrics.lastSuccessUnix.Load() == 0 {
		select {
		case <-deadline:
			t.Fatalf(
				"renewal recovery metrics did not settle: consecutive=%d last_success=%d",
				metrics.consecutiveFailures.Load(), metrics.lastSuccessUnix.Load(),
			)
		case <-time.After(time.Millisecond):
		}
	}
	if got := testutil.ToFloat64(metrics.attempts.WithLabelValues("error")); got != 1 {
		t.Fatalf("error renewal metric = %v, want 1", got)
	}
	if got := testutil.ToFloat64(metrics.attempts.WithLabelValues("success")); got < 1 {
		t.Fatalf("successful renewal metric = %v, want at least 1", got)
	}
	cancel()
	if err := <-done; err != nil {
		t.Fatal(err)
	}
}
