package actor

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

	"github.com/lightningnetwork/lnd/fn/v2"
	"github.com/stretchr/testify/require"
	"pgregory.net/rapid"
)

// TestFutureAwaitContextCancellation tests that Await respects context
// cancellation if the context is cancelled before the future resolves.
func TestFutureAwaitContextCancellation(t *testing.T) {
	t.Parallel()

	rapid.Check(t, func(t *rapid.T) {
		// Test cancellation when the Await context is cancelled via
		// context.Cancel. The underlying future will not be completed, allowing
		// us to test the cancellation path of Await.
		prom1 := NewPromise[int]()
		fut1 := prom1.Future()
		ctx1, cancel1 := context.WithCancel(context.Background())

		// We'll cancel the future immediately after creating it.
		cancel1()

		result1 := fut1.Await(ctx1)

		require.True(t, result1.IsErr())
		require.ErrorIs(
			t, result1.Err(), context.Canceled,
			"await with immediate cancel",
		)

		// Test cancellation when the Await context times out. The
		// underlying future will also not be completed.
		prom2 := NewPromise[int]()
		fut2 := prom2.Future()

		// Use a very short timeout that will trigger.
		ctx2, cancel2 := context.WithTimeout(
			context.Background(), 1*time.Nanosecond,
		)
		defer cancel2()

		// Await the future; it should fall through to the timeout
		// because the future itself is not completed.
		result2 := fut2.Await(ctx2)

		require.True(t, result2.IsErr())
		require.ErrorIs(
			t, result2.Err(), context.DeadlineExceeded,
			"await with timeout",
		)
	})
}

// TestFutureAwaitFutureCompletes tests that Await returns the future's
// result if the context is not cancelled before the future resolves.
func TestFutureAwaitFutureCompletes(t *testing.T) {
	t.Parallel()

	rapid.Check(t, func(t *rapid.T) {
		valToSet := rapid.Int().Draw(t, "valToSet")

		// With a 50% chance, configure the test to complete the future
		// with an error instead of a successful value.
		var errToSet error
		if rapid.Bool().Draw(t, "have_error") {
			errToSet = fmt.Errorf("err")
		}

		promise := NewPromise[int]()
		fut := promise.Future()

		// Use a background context for Await, as we expect the future
		// to complete normally.
		ctx := context.Background()

		// Complete the future in a separate goroutine to simulate an
		// asynchronous operation.
		go func() {
			if errToSet != nil {
				promise.Complete(fn.Err[int](errToSet))
			} else {
				promise.Complete(fn.Ok(valToSet))
			}
		}()

		// Now we'll wait for the future to complete, then verify below
		// that the result (value or error) is as expected.
		result := fut.Await(ctx)

		if errToSet != nil {
			// If an error was set, verify that Await returns that
			// specific error.
			require.True(t, result.IsErr())
			require.ErrorIs(
				t, result.Err(), errToSet,
				"await with error",
			)
		} else {
			// If no error was set, verify that Await returns the
			// correct value.
			require.False(t, result.IsErr(), "await with value")

			result.WhenOk(func(val int) {
				require.Equal(
					t, valToSet, val, "await with value",
				)
			})
		}
	})
}

// TestFutureThenApplyContextCancellation tests that ThenApply respects its
// context, yielding a context error if cancelled before the original future
// completes.
func TestFutureThenApplyContextCancellation(t *testing.T) {
	t.Parallel()

	rapid.Check(t, func(t *rapid.T) {
		// The original future will not be completed in this test case,
		// allowing us to specifically test the cancellation behavior of
		// the context passed to ThenApply.
		originalPromise := NewPromise[int]()
		originalFut := originalPromise.Future()

		// Create a context for ThenApply and cancel it immediately.
		ctxApply, cancelApply := context.WithCancel(
			context.Background(),
		)
		cancelApply()

		var transformCalled atomic.Bool
		transform := func(i int) int {
			transformCalled.Store(true)
			return i * 2
		}

		// Register the transformation. The ThenApply operation itself
		// will start a goroutine to await the originalFut.
		newFut := originalFut.ThenApply(ctxApply, transform)

		// Await the new (transformed) future. Use a background context
		// for this Await to isolate the test to the cancellation of
		// ctxApply.
		result := newFut.Await(context.Background())

		require.True(t, result.IsErr())
		require.ErrorIs(
			t, result.Err(), context.Canceled,
			"ThenApply with cancelled context",
		)
		require.False(
			t, transformCalled.Load(),
			"ThenApply transform function called despite "+
				"context cancellation",
		)
	})
}

// TestFutureThenApplyOriginalFutureCompletes tests ThenApply's behavior when
// the original future completes (with a value or error) before ThenApply's
// context is cancelled.
func TestFutureThenApplyOriginalFutureCompletes(t *testing.T) {
	t.Parallel()

	rapid.Check(t, func(t *rapid.T) {
		initialVal := rapid.Int().Draw(t, "initialVal")

		// Configure whether the original future completes with an error
		// or a successful value.
		var originalErr error
		if rapid.Bool().Draw(t, "have_error") {
			originalErr = fmt.Errorf("original error")
		}

		originalPromise := NewPromise[int]()
		originalFut := originalPromise.Future()

		// Create a context for ThenApply that should not cancel before
		// the original future completes.
		ctxApply, cancelApply := context.WithTimeout(
			context.Background(), 50*time.Millisecond,
		)
		defer cancelApply()

		var transformCalled atomic.Bool
		transform := func(i int) int {
			transformCalled.Store(true)
			return i * 2
		}

		newFut := originalFut.ThenApply(ctxApply, transform)

		// Complete the original future in a separate goroutine to
		// simulate asynchrony.
		go func() {
			if originalErr != nil {
				originalPromise.Complete(
					fn.Err[int](originalErr),
				)
			} else {
				originalPromise.Complete(fn.Ok(initialVal))
			}
		}()

		// Await our new future which transforms the original future's
		// result. Use a background context for this Await.
		result := newFut.Await(context.Background())

		if originalErr != nil {
			// If the original future had an error, the transformed
			// future should also yield that same error.
			require.True(t, result.IsErr())
			require.ErrorIs(
				t, result.Err(), originalErr,
				"ThenApply with original error",
			)
			require.False(
				t, transformCalled.Load(),
				"ThenApply transform function called despite "+
					"original future having an error",
			)
		} else {
			// If the original future completed successfully, the
			// transformed future should contain the transformed value.
			require.False(
				t, result.IsErr(),
				"ThenApply with original value",
			)
			require.True(
				t, transformCalled.Load(),
				"ThenApply transform function not called for "+
					"successful original future",
			)

			result.WhenOk(func(val int) {
				expectedTransformedVal := initialVal * 2
				require.Equal(
					t, expectedTransformedVal, val,
					"ThenApply with original value",
				)
			})
		}
	})
}

// TestFutureOnCompleteContextCancellation tests that OnComplete's callback
// receives a context error if its context is cancelled before the future
// completes.
func TestFutureOnCompleteContextCancellation(t *testing.T) {
	t.Parallel()

	rapid.Check(t, func(t *rapid.T) {
		// The original future will not complete in this test, allowing
		// us to focus on the cancellation of OnComplete's context.
		originalPromise := NewPromise[int]()
		originalFut := originalPromise.Future()

		// Create a context for OnComplete and cancel it immediately to
		// simulate a premature cancellation.
		ctxComplete, cancelComplete := context.WithCancel(
			context.Background(),
		)
		cancelComplete()

		var wg sync.WaitGroup
		wg.Add(1)
		var (
			callbackInvoked     atomic.Bool
			callbackResultValue fn.Result[int]

			// mu is a mutex to protect callbackResultValue as it's
			// written by the callback goroutine and read by the
			// test goroutine.
			mu sync.Mutex
		)

		// Register an OnComplete callback. The callback itself runs in
		// a new goroutine started by OnComplete.
		originalFut.OnComplete(ctxComplete, func(res fn.Result[int]) {
			mu.Lock()
			callbackResultValue = res
			mu.Unlock()

			callbackInvoked.Store(true)
			wg.Done()
		})

		// Use a wait group and a channel to wait for the callback to
		// be invoked.
		waitChan := make(chan struct{})
		go func() {
			wg.Wait()
			close(waitChan)
		}()

		select {
		// The callback should be invoked, even if with a context error.
		case <-waitChan:
		case <-time.After(50 * time.Millisecond):
			require.Fail(
				t, "OnComplete callback timed out waiting "+
					"for execution after context cancel",
			)
		}

		require.True(
			t, callbackInvoked.Load(),
			"OnComplete callback not invoked",
		)

		mu.Lock()
		defer mu.Unlock()

		// Verify that the callback received a context.Canceled error
		// because its context (ctxComplete) was cancelled.
		require.True(t, callbackResultValue.IsErr())
		require.ErrorIs(
			t, callbackResultValue.Err(), context.Canceled,
			"OnComplete with cancelled context",
		)
	})
}

// TestFutureOnCompleteFutureCompletes tests OnComplete's behavior when the
// future completes (with value or error) before its context is cancelled.
func TestFutureOnCompleteFutureCompletes(t *testing.T) {
	t.Parallel()

	rapid.Check(t, func(t *rapid.T) {
		valToSet := rapid.Int().Draw(t, "valToSet")

		// Configure whether the original future completes with an error
		// or a successful value.
		var originalErr error
		if rapid.Bool().Draw(t, "have_error") {
			originalErr = fmt.Errorf("original error")
		}

		originalPromise := NewPromise[int]()
		originalFut := originalPromise.Future()

		// Use a background context for OnComplete, as we expect the
		// future to complete normally.
		ctxComplete := context.Background()

		var wg sync.WaitGroup
		wg.Add(1)

		var (
			callbackInvoked     atomic.Bool
			callbackResultValue fn.Result[int]
			mu                  sync.Mutex
		)

		// Register an OnComplete callback. This callback will execute
		// once the originalFut completes.
		originalFut.OnComplete(ctxComplete, func(res fn.Result[int]) {
			mu.Lock()
			callbackResultValue = res
			mu.Unlock()

			callbackInvoked.Store(true)

			wg.Done()
		})

		// Complete the original future in a separate goroutine to
		// simulate an asynchronous operation.
		go func() {
			if originalErr != nil {
				originalPromise.Complete(
					fn.Err[int](originalErr),
				)
			} else {
				originalPromise.Complete(fn.Ok(valToSet))
			}
		}()

		// Use a wait group and a channel to wait for the callback's
		// execution.
		waitChan := make(chan struct{})
		go func() {
			wg.Wait()
			close(waitChan)
		}()

		select {
		// The callback should be invoked as the future completes.
		case <-waitChan:
		case <-time.After(50 * time.Millisecond):
			require.Fail(
				t, "OnComplete callback timed out waiting "+
					"for execution",
			)
		}

		require.True(t, callbackInvoked.Load())

		mu.Lock()
		defer mu.Unlock()

		// Verify that the callback received the correct result (either
		// the error or the value from the completed future).
		if originalErr != nil {
			require.True(t, callbackResultValue.IsErr())
			require.ErrorIs(
				t, callbackResultValue.Err(), originalErr,
				"OnComplete with error",
			)
		} else {
			require.False(
				t, callbackResultValue.IsErr(),
				"OnComplete with value",
			)
			callbackResultValue.WhenOk(func(val int) {
				require.Equal(
					t, valToSet, val,
					"OnComplete with value",
				)
			})
		}
	})
}

// TestCompleteWith verifies that CompleteWith resolves a promise with the
// supplied value, that the resolution is immediately visible on the Future, and
// that a second call is a safe no-op (idempotency inherited from Complete).
func TestCompleteWith(t *testing.T) {
	t.Parallel()

	// Normal completion — value should be visible on the future.
	promise := NewPromise[int]()
	CompleteWith(promise, 42)

	result := promise.Future().Await(context.Background())
	require.False(t, result.IsErr())
	result.WhenOk(func(v int) {
		require.Equal(t, 42, v)
	})

	// Second call must be a no-op; the future must still hold 42.
	CompleteWith(promise, 99)

	result2 := promise.Future().Await(context.Background())
	require.False(t, result2.IsErr())
	result2.WhenOk(func(v int) {
		require.Equal(t, 42, v, "second CompleteWith must not overwrite")
	})
}

// TestAwaitFuture verifies that AwaitFuture unpacks a resolved future into a
// (value, nil) pair, that a future completed with fn.Err is reported as a
// (zero, err) pair, and that context cancellation before resolution is
// reported as a (zero, ctx.Err()) pair.
func TestAwaitFuture(t *testing.T) {
	t.Parallel()

	// Resolved future — should return the value with a nil error.
	promise := NewPromise[string]()
	CompleteWith(promise, "hello")

	val, err := AwaitFuture(context.Background(), promise.Future())
	require.NoError(t, err)
	require.Equal(t, "hello", val)

	// Future completed with fn.Err — should surface the error as the
	// second return value with the zero string value.
	sentinel := fmt.Errorf("result-level error")
	errPromise := NewPromise[string]()
	errPromise.Complete(fn.Err[string](sentinel))

	val3, err3 := AwaitFuture(context.Background(), errPromise.Future())
	require.ErrorIs(t, err3, sentinel)
	require.Equal(t, "", val3, "zero value expected on fn.Err result")

	// Cancelled context — should return the zero value and ctx.Err().
	unresolved := NewPromise[string]()
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	val2, err2 := AwaitFuture(ctx, unresolved.Future())
	require.ErrorIs(t, err2, context.Canceled)
	require.Equal(t, "", val2, "zero value expected on cancellation")
}

func TestPromiseCompleteIdempotency(t *testing.T) {
	t.Parallel()

	promise := NewPromise[string]()
	future := promise.Future()

	// First completion should succeed.
	firstResult := fn.Ok("first-value")
	ok := promise.Complete(firstResult)
	require.True(t, ok, "first Complete should return true")

	// Second completion with a different value should be ignored.
	secondResult := fn.Ok("second-value")
	ok = promise.Complete(secondResult)
	require.False(t, ok, "second Complete should return false")

	// Third completion with an error should also be ignored.
	thirdResult := fn.Err[string](fmt.Errorf("should be ignored"))
	ok = promise.Complete(thirdResult)
	require.False(t, ok, "third Complete should return false")

	// The future should contain the first value.
	result := future.Await(context.Background())
	require.False(t, result.IsErr(), "future should not be an error")
	result.WhenOk(func(val string) {
		require.Equal(
			t, "first-value", val,
			"future should contain the first completion value",
		)
	})
}
