package tokens

import (
	"context"
	"math/big"
	"math/rand/v2"
	"slices"
	"testing"
	"time"

	"github.com/lightsparkdev/spark/common/keys"
	tokenpb "github.com/lightsparkdev/spark/proto/spark_token"

	_ "github.com/mattn/go-sqlite3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	sparkpb "github.com/lightsparkdev/spark/proto/spark"
	"github.com/lightsparkdev/spark/so/db"
	"github.com/lightsparkdev/spark/so/ent"
	st "github.com/lightsparkdev/spark/so/ent/schema/schematype"
	"github.com/lightsparkdev/spark/so/entfixtures"
	sparktesting "github.com/lightsparkdev/spark/testing"
)

type queryTokenOutputsTestFixture struct {
	Handler *QueryTokenOutputsHandler
	Ctx     context.Context
	Tx      *ent.Tx
}

func setUpQueryTokenOutputsTestHandler(t *testing.T) *queryTokenOutputsTestFixture {
	t.Helper()
	config := sparktesting.TestConfig(t)
	ctx, _ := db.NewTestSQLiteContext(t)

	handler := &QueryTokenOutputsHandler{
		config:                     config,
		includeExpiredTransactions: true,
	}

	return &queryTokenOutputsTestFixture{
		Handler: handler,
		Ctx:     ctx,
	}
}

// createTestTokenOutputs creates the specified number of token outputs with their required dependencies
// for testing pagination functionality. Returns the created outputs.
func createTestTokenOutputs(t *testing.T, ctx context.Context, tx *ent.Tx, count int, ownerKey keys.Public, tokenCreate *ent.TokenCreate, rng *rand.ChaCha8) []*ent.TokenOutput {
	t.Helper()

	f := entfixtures.New(t, ctx, tx).WithRNG(rng)

	outputSpecs := make([]entfixtures.OutputSpec, count)
	for i := range outputSpecs {
		outputSpecs[i] = entfixtures.OutputSpec{
			Amount: big.NewInt(int64(f.RandomBytes(8)[0])),
			Owner:  ownerKey,
		}
	}

	_, outputs := f.CreateMintTransaction(tokenCreate, outputSpecs, st.TokenTransactionStatusFinalized)
	return outputs
}

func TestExpiredOutputBeforeFinalization(t *testing.T) {
	setup := setUpQueryTokenOutputsTestHandler(t)
	handler := setup.Handler
	ctx := setup.Ctx

	tx, err := ent.GetDbFromContext(ctx)
	require.NoError(t, err)
	rng := rand.NewChaCha8([32]byte{})
	t.Run("return output after transaction has expired in signed state", func(t *testing.T) {
		f := entfixtures.New(t, ctx, tx).WithRNG(rng)

		tokenCreate := f.CreateTokenCreate(st.NetworkRegtest, nil)

		// Create a mint transaction that produces an output we will later spend
		ownerKey := keys.MustGeneratePrivateKeyFromRand(rng).Public()
		amount := big.NewInt(int64(f.RandomBytes(8)[0]))
		_, mintOutputs := f.CreateMintTransaction(
			tokenCreate,
			entfixtures.OutputSpecsWithOwner(ownerKey, amount),
			st.TokenTransactionStatusFinalized,
		)
		mintOutput := mintOutputs[0]

		// Create a transfer transaction (SIGNED & expired) that spends the mint output
		expiredAt := time.Now().Add(-1 * time.Hour)
		transferTx, err := tx.TokenTransaction.Create().
			SetPartialTokenTransactionHash(f.RandomBytes(32)).
			SetFinalizedTokenTransactionHash(f.RandomBytes(32)).
			SetStatus(st.TokenTransactionStatusSigned).
			SetExpiryTime(expiredAt).
			Save(ctx)
		require.NoError(t, err)

		// Update mintOutput to mark it as spent by transferTx
		_, err = mintOutput.Update().
			SetStatus(st.TokenOutputStatusSpentSigned).
			SetOutputSpentTokenTransactionID(transferTx.ID).
			SetSpentTransactionInputVout(0).
			Save(ctx)
		require.NoError(t, err)

		// Create a new output produced by the transferTx
		_ = f.CreateOutputForTransaction(tokenCreate, amount, transferTx, 0)

		outputsResp, err := handler.QueryTokenOutputsToken(ctx, &tokenpb.QueryTokenOutputsRequest{
			OwnerPublicKeys: [][]byte{mintOutput.OwnerPublicKey.Serialize()},
			Network:         sparkpb.Network_REGTEST,
		})
		require.NoError(t, err)

		require.Len(t, outputsResp.OutputsWithPreviousTransactionData, 1)
		assert.Equal(t, mintOutput.ID.String(), outputsResp.OutputsWithPreviousTransactionData[0].Output.GetId())
	})
}

func TestQueryTokenOutputsPagination(t *testing.T) {
	setup := setUpQueryTokenOutputsTestHandler(t)
	handler := setup.Handler
	ctx := setup.Ctx

	tx, err := ent.GetDbFromContext(ctx)
	require.NoError(t, err)

	rng := rand.NewChaCha8([32]byte{})
	ownerKey := keys.MustGeneratePrivateKeyFromRand(rng)

	f := entfixtures.New(t, ctx, tx).WithRNG(rng)
	tokenCreate := f.CreateTokenCreate(st.NetworkRegtest, nil)

	createTestTokenOutputs(t, ctx, tx, 7, ownerKey.Public(), tokenCreate, rng)

	t.Run("forward pagination", func(t *testing.T) {
		// Page size 3, expect 3 pages: 3,3,1
		var all []string
		var afterCursor string

		for page := range 3 {
			req := &tokenpb.QueryTokenOutputsRequest{
				OwnerPublicKeys: [][]byte{ownerKey.Public().Serialize()},
				Network:         sparkpb.Network_REGTEST,
				PageRequest: &sparkpb.PageRequest{
					PageSize:  3,
					Direction: sparkpb.Direction_NEXT,
				},
			}
			if afterCursor != "" {
				req.PageRequest.Cursor = afterCursor
			}

			resp, err := handler.QueryTokenOutputsToken(ctx, req)
			require.NoError(t, err)
			require.NotNil(t, resp.PageResponse)

			for _, output := range resp.OutputsWithPreviousTransactionData {
				all = append(all, output.GetOutput().GetId())
			}

			if page == 0 {
				require.False(t, resp.PageResponse.HasPreviousPage)
			} else {
				require.True(t, resp.PageResponse.HasPreviousPage)
			}

			if page < 2 {
				require.True(t, resp.PageResponse.HasNextPage)
				require.NotEmpty(t, resp.PageResponse.NextCursor)
				afterCursor = resp.PageResponse.NextCursor
			} else {
				require.False(t, resp.PageResponse.HasNextPage)
			}
		}

		assert.Len(t, all, 7)
		assert.True(t, slices.IsSorted(all), "IDs should be sorted ascending by UUID; got %v", all)
	})

	t.Run("backward pagination", func(t *testing.T) {
		resp, err := handler.QueryTokenOutputsToken(ctx, &tokenpb.QueryTokenOutputsRequest{
			OwnerPublicKeys: [][]byte{ownerKey.Public().Serialize()},
			Network:         sparkpb.Network_REGTEST,
			PageRequest: &sparkpb.PageRequest{
				PageSize:  DefaultTokenOutputPageSize,
				Direction: sparkpb.Direction_NEXT,
			},
		})
		require.NoError(t, err)
		assert.Len(t, resp.OutputsWithPreviousTransactionData, 7)

		// Use the PageResponse.NextCursor from the response, which is properly encoded
		beforeCursor := resp.PageResponse.NextCursor
		require.NotEmpty(t, beforeCursor)

		// Backward pagination should return an error
		_, err = handler.QueryTokenOutputsToken(ctx, &tokenpb.QueryTokenOutputsRequest{
			OwnerPublicKeys: [][]byte{ownerKey.Public().Serialize()},
			Network:         sparkpb.Network_REGTEST,
			PageRequest: &sparkpb.PageRequest{
				PageSize:  3,
				Cursor:    beforeCursor,
				Direction: sparkpb.Direction_PREVIOUS,
			},
		})
		require.ErrorContains(t, err, "backward pagination with 'previous' direction is not currently supported")
	})

	t.Run("default page size", func(t *testing.T) {
		resp, err := handler.QueryTokenOutputsToken(ctx, &tokenpb.QueryTokenOutputsRequest{
			OwnerPublicKeys: [][]byte{ownerKey.Public().Serialize()},
			Network:         sparkpb.Network_REGTEST,
			// PageRequest not set, should use DefaultTokenOutputPageSize
		})
		require.NoError(t, err)
		require.NotNil(t, resp.PageResponse)

		// Should get all 7 outputs since DefaultTokenOutputPageSize (500) > 7
		assert.Len(t, resp.OutputsWithPreviousTransactionData, 7)
		assert.False(t, resp.PageResponse.HasNextPage)
		assert.False(t, resp.PageResponse.HasPreviousPage)
	})

	t.Run("page size limit", func(t *testing.T) {
		resp, err := handler.QueryTokenOutputsToken(ctx, &tokenpb.QueryTokenOutputsRequest{
			OwnerPublicKeys: [][]byte{ownerKey.Public().Serialize()},
			Network:         sparkpb.Network_REGTEST,
			PageRequest: &sparkpb.PageRequest{
				PageSize:  MaxTokenOutputPageSize + 100,
				Direction: sparkpb.Direction_NEXT,
			},
		})
		require.NoError(t, err)

		assert.Len(t, resp.OutputsWithPreviousTransactionData, 7)
	})
}
