package contractcourt

import (
	"sync"
	"testing"
	"time"

	"github.com/btcsuite/btcd/chainhash/v2"
	"github.com/btcsuite/btcd/wire/v2"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/fn/v2"
	lnmock "github.com/lightningnetwork/lnd/lntest/mock"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwire"
)

// testReporter is a minimal interface for test reporting that is satisfied
// by both *testing.T and *rapid.T, allowing the harness to work with
// property-based tests.
type testReporter interface {
	Helper()
	Fatalf(format string, args ...any)
}

// chainWatcherTestHarness provides a test harness for chain watcher tests
// with utilities for simulating spends, confirmations, and reorganizations.
type chainWatcherTestHarness struct {
	t testReporter

	// aliceChannel and bobChannel are the test channels.
	aliceChannel *lnwallet.LightningChannel
	bobChannel   *lnwallet.LightningChannel

	// chainWatcher is the chain watcher under test.
	chainWatcher *chainWatcher

	// notifier is the mock chain notifier.
	notifier *mockChainNotifier

	// chanEvents is the channel event subscription.
	chanEvents *ChainEventSubscription

	// currentHeight tracks the current block height.
	currentHeight int32

	// blockbeatProcessed is a channel that signals when a blockbeat has
	// been processed.
	blockbeatProcessed chan struct{}

	// earlyCoopCloseMu guards earlyCoopCloseSummaries.
	earlyCoopCloseMu sync.Mutex

	// earlyCoopCloseSummaries records every invocation of the
	// notifyEarlyCoopClose callback when captureEarlyCoopClose was
	// enabled. Tests assert against length and contents.
	earlyCoopCloseSummaries []*channeldb.ChannelCloseSummary
}

// mockChainNotifier extends the standard mock with additional channels for
// testing cooperative close reorgs.
type mockChainNotifier struct {
	*lnmock.ChainNotifier

	// confEvents tracks active confirmation event subscriptions.
	confEvents []*mockConfirmationEvent

	// confRegistered is a channel that signals when a new confirmation
	// event has been registered.
	confRegistered chan struct{}

	// spendEvents tracks active spend event subscriptions.
	spendEvents []*chainntnfs.SpendEvent

	// spendRegistered is a channel that signals when a new spend
	// event has been registered.
	spendRegistered chan struct{}
}

// mockConfirmationEvent represents a mock confirmation event subscription.
type mockConfirmationEvent struct {
	txid          chainhash.Hash
	numConfs      uint32
	confirmedChan chan *chainntnfs.TxConfirmation
	negConfChan   chan int32
	cancelled     bool
}

// RegisterSpendNtfn creates a new mock spend event.
func (m *mockChainNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
	pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {

	// The base mock already has SpendChan, use that.
	spendEvent := &chainntnfs.SpendEvent{
		Spend: m.SpendChan,
		Cancel: func() {
			// No-op for now.
		},
	}

	m.spendEvents = append(m.spendEvents, spendEvent)

	// Signal that a new spend event has been registered.
	select {
	case m.spendRegistered <- struct{}{}:
	default:
	}

	return spendEvent, nil
}

// RegisterConfirmationsNtfn creates a new mock confirmation event.
func (m *mockChainNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
	pkScript []byte, numConfs, heightHint uint32,
	opts ...chainntnfs.NotifierOption,
) (*chainntnfs.ConfirmationEvent, error) {

	mockEvent := &mockConfirmationEvent{
		txid:          *txid,
		numConfs:      numConfs,
		confirmedChan: make(chan *chainntnfs.TxConfirmation, 1),
		negConfChan:   make(chan int32, 1),
	}

	m.confEvents = append(m.confEvents, mockEvent)

	// Signal that a new confirmation event has been registered.
	select {
	case m.confRegistered <- struct{}{}:
	default:
	}

	return &chainntnfs.ConfirmationEvent{
		Confirmed:    mockEvent.confirmedChan,
		NegativeConf: mockEvent.negConfChan,
		Cancel: func() {
			mockEvent.cancelled = true
		},
	}, nil
}

// harnessOpt is a functional option for configuring the test harness.
type harnessOpt func(*harnessConfig)

// harnessConfig holds configuration for the test harness.
type harnessConfig struct {
	requiredConfs fn.Option[uint32]

	// captureEarlyCoopClose, when true, wires a notifyEarlyCoopClose
	// callback into the chain watcher that records each invocation onto
	// the harness's earlyCoopCloseSummaries slice for assertions.
	captureEarlyCoopClose bool
}

// withRequiredConfs sets the number of confirmations required for channel
// closes.
func withRequiredConfs(confs uint32) harnessOpt {
	return func(cfg *harnessConfig) {
		cfg.requiredConfs = fn.Some(confs)
	}
}

// withEarlyCoopCloseCapture enables recording of every invocation of the
// chain watcher's notifyEarlyCoopClose callback on the harness so tests can
// assert when, how often, and with which summary the early-dispatch fires.
func withEarlyCoopCloseCapture() harnessOpt {
	return func(cfg *harnessConfig) {
		cfg.captureEarlyCoopClose = true
	}
}

// newChainWatcherTestHarness creates a new test harness for chain watcher
// tests.
func newChainWatcherTestHarness(t *testing.T,
	opts ...harnessOpt) *chainWatcherTestHarness {

	return newChainWatcherTestHarnessFromReporter(t, t, opts...)
}

// newChainWatcherTestHarnessFromReporter creates a test harness that works
// with both *testing.T and *rapid.T. The t parameter is used for
// operations that specifically require *testing.T (like CreateTestChannels),
// while reporter is used for all test reporting (Helper, Fatalf).
func newChainWatcherTestHarnessFromReporter(t *testing.T,
	reporter testReporter, opts ...harnessOpt) *chainWatcherTestHarness {

	reporter.Helper()

	// Apply options.
	cfg := &harnessConfig{
		requiredConfs: fn.None[uint32](),
	}
	for _, opt := range opts {
		opt(cfg)
	}

	// Create test channels.
	aliceChannel, bobChannel, err := lnwallet.CreateTestChannels(
		t, channeldb.SingleFunderTweaklessBit,
	)
	if err != nil {
		reporter.Fatalf("unable to create test channels: %v", err)
	}

	// Create mock notifier.
	baseNotifier := &lnmock.ChainNotifier{
		SpendChan: make(chan *chainntnfs.SpendDetail, 1),
		EpochChan: make(chan *chainntnfs.BlockEpoch),
		ConfChan:  make(chan *chainntnfs.TxConfirmation, 1),
	}

	notifier := &mockChainNotifier{
		ChainNotifier:   baseNotifier,
		confEvents:      make([]*mockConfirmationEvent, 0),
		confRegistered:  make(chan struct{}, 10),
		spendEvents:     make([]*chainntnfs.SpendEvent, 0),
		spendRegistered: make(chan struct{}, 10),
	}

	harness := &chainWatcherTestHarness{
		t:                  reporter,
		aliceChannel:       aliceChannel,
		bobChannel:         bobChannel,
		notifier:           notifier,
		currentHeight:      100,
		blockbeatProcessed: make(chan struct{}),
	}

	// If the test wants to observe early-dispatch invocations, install
	// a callback that records each summary onto the harness.
	var notifyEarlyCoopClose func(*channeldb.ChannelCloseSummary)
	if cfg.captureEarlyCoopClose {
		notifyEarlyCoopClose = func(
			s *channeldb.ChannelCloseSummary) {

			harness.earlyCoopCloseMu.Lock()
			harness.earlyCoopCloseSummaries = append(
				harness.earlyCoopCloseSummaries, s,
			)
			harness.earlyCoopCloseMu.Unlock()
		}
	}

	// Create chain watcher.
	chainWatcher, err := newChainWatcher(chainWatcherConfig{
		chanState:            aliceChannel.State(),
		notifier:             notifier,
		signer:               aliceChannel.Signer,
		extractStateNumHint:  lnwallet.GetStateNumHint,
		chanCloseConfs:       cfg.requiredConfs,
		notifyEarlyCoopClose: notifyEarlyCoopClose,
		contractBreach: func(
			retInfo *lnwallet.BreachRetribution,
		) error {
			// In tests, we just need to accept the breach
			// notification.
			return nil
		},
	})
	if err != nil {
		reporter.Fatalf("unable to create chain watcher: %v", err)
	}

	// Start chain watcher (this will register for spend notification).
	err = chainWatcher.Start()
	if err != nil {
		reporter.Fatalf("unable to start chain watcher: %v", err)
	}

	// Subscribe to channel events.
	chanEvents := chainWatcher.SubscribeChannelEvents()

	harness.chainWatcher = chainWatcher
	harness.chanEvents = chanEvents

	// Wait for the initial spend registration that happens in Start().
	harness.waitForSpendRegistration()

	// Verify BlockbeatChan is initialized.
	if chainWatcher.BlockbeatChan == nil {
		reporter.Fatalf("BlockbeatChan is nil after initialization")
	}

	// Register cleanup. We use the t for Cleanup since rapid.T
	// may not have this method in the same way.
	t.Cleanup(func() {
		_ = chainWatcher.Stop()
	})

	return harness
}

// createCoopCloseTx creates a cooperative close transaction with the given
// output value. The transaction will have the proper sequence number to
// indicate it's a cooperative close.
func (h *chainWatcherTestHarness) createCoopCloseTx(
	outputValue int64) *wire.MsgTx {

	fundingOutpoint := h.aliceChannel.State().FundingOutpoint

	return &wire.MsgTx{
		TxIn: []*wire.TxIn{{
			PreviousOutPoint: fundingOutpoint,
			Sequence:         wire.MaxTxInSequenceNum,
		}},
		TxOut: []*wire.TxOut{{
			Value: outputValue,
			// Unique script.
			PkScript: []byte{byte(outputValue % 255)},
		}},
	}
}

// createRemoteForceCloseTx creates a remote force close transaction.
// From Alice's perspective, this is Bob's local commitment transaction.
func (h *chainWatcherTestHarness) createRemoteForceCloseTx() *wire.MsgTx {
	return h.bobChannel.State().LocalCommitment.CommitTx
}

// createLocalForceCloseTx creates a local force close transaction.
// This is Alice's local commitment transaction.
func (h *chainWatcherTestHarness) createLocalForceCloseTx() *wire.MsgTx {
	return h.aliceChannel.State().LocalCommitment.CommitTx
}

// createBreachCloseTx creates a breach (revoked commitment) transaction.
// We advance the channel state, save the commitment, then advance again
// to revoke it. Returns the revoked commitment tx.
func (h *chainWatcherTestHarness) createBreachCloseTx() *wire.MsgTx {
	h.t.Helper()

	// To create a revoked commitment, we need to advance the channel state
	// at least once. We'll use the test utils helper to add an HTLC and
	// force a state transition.

	// Get the current commitment before we advance (this will be revoked).
	revokedCommit := h.bobChannel.State().LocalCommitment.CommitTx

	// Add a fake HTLC to advance state.
	htlcAmount := lnwire.NewMSatFromSatoshis(10000)
	paymentHash := [32]byte{4, 5, 6}
	htlc := &lnwire.UpdateAddHTLC{
		ID:          0,
		Amount:      htlcAmount,
		Expiry:      uint32(h.currentHeight + 100),
		PaymentHash: paymentHash,
	}

	// Add HTLC to both channels.
	if _, err := h.aliceChannel.AddHTLC(htlc, nil); err != nil {
		h.t.Fatalf("unable to add HTLC to alice: %v", err)
	}
	if _, err := h.bobChannel.ReceiveHTLC(htlc); err != nil {
		h.t.Fatalf("unable to add HTLC to bob: %v", err)
	}

	// Force state transition using the helper.
	err := lnwallet.ForceStateTransition(h.aliceChannel, h.bobChannel)
	if err != nil {
		h.t.Fatalf("unable to force state transition: %v", err)
	}

	// Return the revoked commitment (Bob's previous local commitment).
	return revokedCommit
}

// sendSpend sends a spend notification for the given transaction.
func (h *chainWatcherTestHarness) sendSpend(tx *wire.MsgTx) {
	h.t.Helper()

	txHash := tx.TxHash()
	spend := &chainntnfs.SpendDetail{
		SpenderTxHash:  &txHash,
		SpendingTx:     tx,
		SpendingHeight: h.currentHeight,
	}

	select {
	case h.notifier.SpendChan <- spend:
	case <-time.After(time.Second):
		h.t.Fatalf("unable to send spend notification")
	}
}

// confirmTx sends a confirmation notification for the given transaction.
func (h *chainWatcherTestHarness) confirmTx(tx *wire.MsgTx, height int32) {
	h.t.Helper()

	// Find the confirmation event for this transaction.
	txHash := tx.TxHash()
	var confEvent *mockConfirmationEvent
	for _, event := range h.notifier.confEvents {
		if event.txid == txHash && !event.cancelled {
			confEvent = event
			break
		}
	}

	if confEvent == nil {
		h.t.Fatalf("no confirmation event registered for tx %v", txHash)
	}

	// Send confirmation.
	select {
	case confEvent.confirmedChan <- &chainntnfs.TxConfirmation{
		Tx:          tx,
		BlockHeight: uint32(height),
	}:
	case <-time.After(time.Second):
		h.t.Fatalf("unable to send confirmation")
	}
}

// triggerReorg sends a negative confirmation (reorg) notification for the
// given transaction with the specified reorg depth.
func (h *chainWatcherTestHarness) triggerReorg(tx *wire.MsgTx,
	reorgDepth int32) {

	h.t.Helper()

	// Find the confirmation event for this transaction.
	txHash := tx.TxHash()
	var confEvent *mockConfirmationEvent
	for _, event := range h.notifier.confEvents {
		if event.txid == txHash && !event.cancelled {
			confEvent = event
			break
		}
	}

	if confEvent == nil {
		// The chain watcher might not have registered for
		// confirmations yet.
		return
	}

	// Send negative confirmation.
	select {
	case confEvent.negConfChan <- reorgDepth:
	case <-time.After(time.Second):
		h.t.Fatalf("unable to send negative confirmation")
	}
}

// mineBlocks advances the current block height.
func (h *chainWatcherTestHarness) mineBlocks(n int32) {
	h.currentHeight += n
}

// earlyCoopCloseCount returns how many times the early-dispatch callback has
// fired since the harness started.
func (h *chainWatcherTestHarness) earlyCoopCloseCount() int {
	h.earlyCoopCloseMu.Lock()
	defer h.earlyCoopCloseMu.Unlock()

	return len(h.earlyCoopCloseSummaries)
}

// earlyCoopCloseAt returns the early-dispatch summary recorded at the given
// index. The harness fails the test if the index is out of range.
func (h *chainWatcherTestHarness) earlyCoopCloseAt(
	idx int) *channeldb.ChannelCloseSummary {

	h.earlyCoopCloseMu.Lock()
	defer h.earlyCoopCloseMu.Unlock()

	if idx >= len(h.earlyCoopCloseSummaries) {
		h.t.Fatalf("expected early-dispatch index %d, only %d "+
			"summaries recorded", idx,
			len(h.earlyCoopCloseSummaries))
	}

	return h.earlyCoopCloseSummaries[idx]
}

// waitForEarlyCoopClose blocks until at least the supplied count of
// early-dispatch invocations have been recorded, or the timeout elapses.
func (h *chainWatcherTestHarness) waitForEarlyCoopClose(want int,
	timeout time.Duration) {

	h.t.Helper()

	deadline := time.Now().Add(timeout)
	for {
		if h.earlyCoopCloseCount() >= want {
			return
		}
		if time.Now().After(deadline) {
			h.t.Fatalf("expected %d early-dispatch invocations, "+
				"got %d after %v", want,
				h.earlyCoopCloseCount(), timeout)

			return
		}

		time.Sleep(10 * time.Millisecond)
	}
}

// waitForCoopClose waits for a cooperative close event and returns it.
func (h *chainWatcherTestHarness) waitForCoopClose(
	timeout time.Duration) *CooperativeCloseInfo {

	h.t.Helper()

	select {
	case coopClose := <-h.chanEvents.CooperativeClosure:
		return coopClose
	case <-time.After(timeout):
		h.t.Fatalf("didn't receive cooperative close event")
		return nil
	}
}

// waitForConfRegistration waits for the chain watcher to register for
// confirmation notifications.
func (h *chainWatcherTestHarness) waitForConfRegistration() {
	h.t.Helper()

	select {
	case <-h.notifier.confRegistered:
		// Registration complete.
	case <-time.After(2 * time.Second):
		// Not necessarily a failure - some tests don't register.
	}
}

// waitForSpendRegistration waits for the chain watcher to register for
// spend notifications.
func (h *chainWatcherTestHarness) waitForSpendRegistration() {
	h.t.Helper()

	select {
	case <-h.notifier.spendRegistered:
		// Registration complete.
	case <-time.After(2 * time.Second):
		// Not necessarily a failure - some tests don't register.
	}
}

// assertCoopCloseTx asserts that the given cooperative close info matches
// the expected transaction.
func (h *chainWatcherTestHarness) assertCoopCloseTx(
	closeInfo *CooperativeCloseInfo, expectedTx *wire.MsgTx) {

	h.t.Helper()

	expectedHash := expectedTx.TxHash()
	if closeInfo.ClosingTXID != expectedHash {
		h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
			expectedHash, closeInfo.ClosingTXID)
	}
}

// assertNoCoopClose asserts that no cooperative close event is received
// within the given timeout.
func (h *chainWatcherTestHarness) assertNoCoopClose(timeout time.Duration) {
	h.t.Helper()

	select {
	case <-h.chanEvents.CooperativeClosure:
		h.t.Fatalf("unexpected cooperative close event")
	case <-time.After(timeout):
		// Expected timeout.
	}
}

// runCoopCloseFlow runs a complete cooperative close flow including spend,
// optional reorg, and confirmation. This helper coordinates the timing
// between the different events.
func (h *chainWatcherTestHarness) runCoopCloseFlow(
	tx *wire.MsgTx, shouldReorg bool, reorgDepth int32,
	altTx *wire.MsgTx) *CooperativeCloseInfo {

	h.t.Helper()

	// Send initial spend notification. The closeObserver's state machine
	// will detect this and register for confirmations.
	h.sendSpend(tx)

	// Wait for the chain watcher to register for confirmations.
	h.waitForConfRegistration()

	if shouldReorg {
		// Trigger reorg which resets the state machine.
		h.triggerReorg(tx, reorgDepth)

		// If we have an alternative transaction, send it.
		if altTx != nil {
			// After reorg, the chain watcher should re-register for
			// ANY spend of the funding output.
			h.waitForSpendRegistration()

			// Send alternative spend.
			h.sendSpend(altTx)

			// Wait for it to register for confirmations.
			h.waitForConfRegistration()

			// Confirm alternative transaction to unblock.
			h.mineBlocks(1)
			h.confirmTx(altTx, h.currentHeight)
		}
	} else {
		// Normal confirmation flow - confirm to unblock
		// waitForCoopCloseConfirmation.
		h.mineBlocks(1)
		h.confirmTx(tx, h.currentHeight)
	}

	// Wait for cooperative close event.
	return h.waitForCoopClose(5 * time.Second)
}

// runMultipleReorgFlow simulates multiple consecutive reorganizations with
// different transactions confirming after each reorg.
func (h *chainWatcherTestHarness) runMultipleReorgFlow(txs []*wire.MsgTx,
	reorgDepths []int32) *CooperativeCloseInfo {

	h.t.Helper()

	if len(txs) < 2 {
		h.t.Fatalf("need at least 2 transactions for reorg flow")
	}
	if len(reorgDepths) != len(txs)-1 {
		h.t.Fatalf("reorg depths must be one less than transactions")
	}

	// Send initial spend.
	h.sendSpend(txs[0])

	// Process each reorg.
	for i, depth := range reorgDepths {
		// Wait for confirmation registration.
		h.waitForConfRegistration()

		// Trigger reorg for current transaction.
		h.triggerReorg(txs[i], depth)

		// Wait for re-registration for spend.
		h.waitForSpendRegistration()

		// Send next transaction.
		h.sendSpend(txs[i+1])
	}

	// Wait for final confirmation registration.
	h.waitForConfRegistration()

	// Confirm the final transaction.
	finalTx := txs[len(txs)-1]
	h.mineBlocks(1)
	h.confirmTx(finalTx, h.currentHeight)

	// Wait for cooperative close event.
	return h.waitForCoopClose(10 * time.Second)
}

// waitForRemoteUnilateralClose waits for a remote unilateral close event.
func (h *chainWatcherTestHarness) waitForRemoteUnilateralClose(
	timeout time.Duration) *RemoteUnilateralCloseInfo {

	h.t.Helper()

	select {
	case remoteClose := <-h.chanEvents.RemoteUnilateralClosure:
		return remoteClose
	case <-time.After(timeout):
		h.t.Fatalf("didn't receive remote unilateral close event")
		return nil
	}
}

// waitForLocalUnilateralClose waits for a local unilateral close event.
func (h *chainWatcherTestHarness) waitForLocalUnilateralClose(
	timeout time.Duration) *LocalUnilateralCloseInfo {

	h.t.Helper()

	select {
	case localClose := <-h.chanEvents.LocalUnilateralClosure:
		return localClose
	case <-time.After(timeout):
		h.t.Fatalf("didn't receive local unilateral close event")
		return nil
	}
}

// waitForBreach waits for a breach (contract breach) event.
func (h *chainWatcherTestHarness) waitForBreach(
	timeout time.Duration) *BreachCloseInfo {

	h.t.Helper()

	select {
	case breach := <-h.chanEvents.ContractBreach:
		return breach
	case <-time.After(timeout):
		h.t.Fatalf("didn't receive contract breach event")
		return nil
	}
}

// assertRemoteUnilateralCloseTx asserts that the given remote unilateral close
// info matches the expected transaction.
func (h *chainWatcherTestHarness) assertRemoteUnilateralCloseTx(
	closeInfo *RemoteUnilateralCloseInfo, expectedTx *wire.MsgTx) {

	h.t.Helper()

	expectedHash := expectedTx.TxHash()
	actualHash := closeInfo.UnilateralCloseSummary.SpendDetail.SpenderTxHash
	if *actualHash != expectedHash {
		h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
			expectedHash, *actualHash)
	}
}

// assertLocalUnilateralCloseTx asserts that the given local unilateral close
// info matches the expected transaction.
func (h *chainWatcherTestHarness) assertLocalUnilateralCloseTx(
	closeInfo *LocalUnilateralCloseInfo, expectedTx *wire.MsgTx) {

	h.t.Helper()

	expectedHash := expectedTx.TxHash()
	actualHash := closeInfo.LocalForceCloseSummary.CloseTx.TxHash()
	if actualHash != expectedHash {
		h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
			expectedHash, actualHash)
	}
}

// assertBreachTx asserts that the given breach info matches the expected
// transaction.
func (h *chainWatcherTestHarness) assertBreachTx(
	breachInfo *BreachCloseInfo, expectedTx *wire.MsgTx) {

	h.t.Helper()

	expectedHash := expectedTx.TxHash()
	if breachInfo.CommitHash != expectedHash {
		h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
			expectedHash, breachInfo.CommitHash)
	}
}
