// Package aerospike provides an Aerospike-based implementation of the UTXO store interface.
// It offers high performance, distributed storage capabilities with support for large-scale
// UTXO sets and complex operations like freezing, reassignment, and batch processing.
//
// # Architecture
//
// The implementation uses a combination of Aerospike Key-Value store and Lua scripts
// for atomic operations. Transactions are stored with the following structure:
//   - Main Record: Contains transaction metadata and up to 20,000 UTXOs
//   - Pagination Records: Additional records for transactions with >20,000 outputs
//   - External Storage: Optional blob storage for large transactions
//
// # Features
//
//   - Efficient UTXO lifecycle management (create, spend, unspend)
//   - Support for batched operations with LUA scripting
//   - Automatic cleanup of spent UTXOs through DAH
//   - Alert system integration for freezing/unfreezing UTXOs
//   - Metrics tracking via Prometheus
//   - Support for large transactions through external blob storage
//
// # Usage
//
//	store, err := aerospike.New(ctx, logger, settings, &url.URL{
//	    Scheme: "aerospike",
//	    Host:   "localhost:3000",
//	    Path:   "/test/utxos",
//	    RawQuery: "expiration=3600&set=txmeta",
//	})
//
// # Database Structure
//
// Normal Transaction:
//   - inputs: Transaction input data
//   - outputs: Transaction output data
//   - utxos: List of UTXO hashes
//   - totalUtxos: Total number of UTXOs
//   - spentUtxos: Number of spent UTXOs
//   - blockIDs: Block references
//   - isCoinbase: Coinbase flag
//   - spendingHeight: Coinbase maturity height
//   - frozen: Frozen status
//
// Large Transaction with External Storage:
//   - Same as normal but with external=true
//   - Transaction data stored in blob storage
//   - Multiple records for >20k outputs
//
// # Thread Safety
//
// The implementation is fully thread-safe and supports concurrent access through:
//   - Atomic operations via Lua scripts
//   - Batched operations for better performance
//   - Lock-free reads with optimistic concurrency
package aerospike

import (
	"context"
	"encoding/binary"
	"fmt"
	"net/url"
	"os"
	"sync"
	"testing"
	"time"

	"github.com/aerospike/aerospike-client-go/v8"
	aeroTest "github.com/bitcoin-sv/testcontainers-aerospike-go"
	"github.com/bsv-blockchain/go-bt/v2"
	"github.com/bsv-blockchain/go-bt/v2/chainhash"
	"github.com/bsv-blockchain/teranode/stores/utxo"
	"github.com/bsv-blockchain/teranode/stores/utxo/fields"
	"github.com/bsv-blockchain/teranode/ulogger"
	"github.com/bsv-blockchain/teranode/util/test"
	"github.com/bsv-blockchain/teranode/util/uaerospike"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCalculateKeySource(t *testing.T) {
	hash := chainhash.HashH([]byte("test"))

	batchSize := 1

	h := uaerospike.CalculateKeySource(&hash, 0, batchSize)
	assert.Equal(t, hash[:], h)

	h = uaerospike.CalculateKeySource(&hash, 1, batchSize)
	extra := make([]byte, 4)
	binary.LittleEndian.PutUint32(extra, uint32(1))
	assert.Equal(t, append(hash[:], extra...), h)

	h = uaerospike.CalculateKeySource(&hash, 2, batchSize)
	extra = make([]byte, 4)
	binary.LittleEndian.PutUint32(extra, uint32(2))
	assert.Equal(t, append(hash[:], extra...), h)
}

func TestCalculateOffsetOutput(t *testing.T) {
	db := &Store{utxoBatchSize: 20_000}

	offset := db.calculateOffsetForOutput(0)
	assert.Equal(t, uint32(0), offset)

	offset = db.calculateOffsetForOutput(30_000)
	assert.Equal(t, uint32(10_000), offset)

	offset = db.calculateOffsetForOutput(40_000)
	assert.Equal(t, uint32(0), offset)
}

func TestUnmined(t *testing.T) {
	logger := ulogger.NewErrorTestLogger(t)
	ctx := context.Background()

	tSettings := test.CreateBaseTestSettings(t)

	container, err := aeroTest.RunContainer(ctx)
	require.NoError(t, err)

	t.Cleanup(func() {
		err = container.Terminate(ctx)
		require.NoError(t, err)
	})

	host, err := container.Host(ctx)
	require.NoError(t, err)

	port, err := container.ServicePort(ctx)
	require.NoError(t, err)

	client, err := uaerospike.NewClient(host, port)
	require.NoError(t, err)

	aeroURL, err := url.Parse(fmt.Sprintf("aerospike://%s:%d/test?set=utxo&externalStore=file://./data/external", host, port))
	require.NoError(t, err)

	store, err := New(ctx, logger, tSettings, aeroURL)
	require.NoError(t, err)

	t.Run("check_empty_store", func(t *testing.T) {
		exists, err := store.indexExists("unminedSinceIndex")
		require.NoError(t, err)
		assert.True(t, exists)

		stmt := aerospike.NewStatement(store.namespace, store.setName)

		err = stmt.SetFilter(aerospike.NewRangeFilter(fields.UnminedSince.String(), 1, int64(4294967295)))
		require.NoError(t, err)

		recordset, err := client.Query(nil, stmt)
		require.NoError(t, err)

		count := 0

		for range recordset.Records() {
			count++
		}

		assert.Equal(t, count, 0)
	})

	t.Run("check_not_mined_tx", func(t *testing.T) {
		currentBlockHeight := uint32(1)

		txUnMined, err := bt.NewTxFromString("010000000000000000ef011c044c4db32b3da68aa54e3f30c71300db250e0b48ea740bd3897a8ea1a2cc9a020000006b483045022100c6177fa406ecb95817d3cdd3e951696439b23f8e888ef993295aa73046504029022052e75e7bfd060541be406ec64f4fc55e708e55c3871963e95bf9bd34df747ee041210245c6e32afad67f6177b02cfc2878fce2a28e77ad9ecbc6356960c020c592d867ffffffffd4c7a70c000000001976a914296b03a4dd56b3b0fe5706c845f2edff22e84d7388ac0301000000000000001976a914a4429da7462800dedc7b03a4fc77c363b8de40f588ac000000000000000024006a4c2042535620466175636574207c20707573682d7468652d627574746f6e2e617070d2c7a70c000000001976a914296b03a4dd56b3b0fe5706c845f2edff22e84d7388ac00000000")
		require.NoError(t, err)

		t.Logf("Unmined txid: %v", txUnMined.TxIDChainHash())

		_, err = store.Create(store.ctx, txUnMined, currentBlockHeight)
		require.NoError(t, err)

		txMined := txUnMined.Clone()
		txMined.Version++

		t.Logf("Mined txid: %v", txMined.TxIDChainHash())

		_, err = store.Create(store.ctx, txMined, currentBlockHeight, utxo.WithMinedBlockInfo(
			utxo.MinedBlockInfo{
				BlockID:     1,
				BlockHeight: currentBlockHeight,
				SubtreeIdx:  1,
			},
		))
		require.NoError(t, err)

		exists, err := store.indexExists("unminedSinceIndex")
		require.NoError(t, err)
		assert.True(t, exists)

		stmt := aerospike.NewStatement(store.namespace, store.setName)

		err = stmt.SetFilter(aerospike.NewRangeFilter(fields.UnminedSince.String(), int64(currentBlockHeight), int64(4294967295)))
		require.NoError(t, err)

		recordset, err := client.Query(nil, stmt)
		require.NoError(t, err)

		count := 0

		for rec := range recordset.Records() {
			assert.NotNil(t, rec.Bins[fields.UnminedSince.String()])
			assert.Equal(t, int(currentBlockHeight), rec.Bins[fields.UnminedSince.String()])
			assert.Equal(t, txUnMined.TxIDChainHash().CloneBytes(), rec.Bins[fields.TxID.String()])

			count++
		}

		assert.Equal(t, 1, count)

		// set the tx as mined
		blockIDsMap, err := store.SetMinedMulti(store.ctx, []*chainhash.Hash{txUnMined.TxIDChainHash()}, utxo.MinedBlockInfo{
			BlockID:        1,
			BlockHeight:    currentBlockHeight,
			SubtreeIdx:     1,
			OnLongestChain: true,
		})
		require.NoError(t, err)
		require.Len(t, blockIDsMap, 1)
		require.Len(t, blockIDsMap[*txUnMined.TxIDChainHash()], 1)
		require.Equal(t, []uint32{1}, blockIDsMap[*txUnMined.TxIDChainHash()])

		recordset, err = client.Query(nil, stmt)
		require.NoError(t, err)

		count = 0

		for rec := range recordset.Records() {
			assert.NotNil(t, rec.Bins[fields.UnminedSince.String()])
			assert.Equal(t, int(currentBlockHeight), rec.Bins[fields.UnminedSince.String()])
			assert.Equal(t, txUnMined.TxIDChainHash().CloneBytes(), rec.Bins[fields.TxID.String()])

			count++
		}

		assert.Equal(t, 0, count)
	})

	t.Run("check_not_on_longest_chain_mined_tx", func(t *testing.T) {
		currentBlockHeight := uint32(1)

		txUnMined, err := bt.NewTxFromString("010000000000000000ef011c044c4db32b3da68aa54e3f30c71300db250e0b48ea740bd3897a8ea1a2cc9a020000006b483045022100c6177fa406ecb95817d3cdd3e951696439b23f8e888ef993295aa73046504029022052e75e7bfd060541be406ec64f4fc55e708e55c3871963e95bf9bd34df747ee041210245c6e32afad67f6177b02cfc2878fce2a28e77ad9ecbc6356960c020c592d867ffffffffd4c7a70c000000001976a914296b03a4dd56b3b0fe5706c845f2edff22e84d7388ac0301000000000000001976a914a4429da7462800dedc7b03a4fc77c363b8de40f588ac000000000000000024006a4c2042535620466175636574207c20707573682d7468652d627574746f6e2e617070d2c7a70c000000001976a914296b03a4dd56b3b0fe5706c845f2edff22e84d7388ac00000000")
		require.NoError(t, err)
		txUnMined.Version = 4

		t.Logf("Unmined txid: %v", txUnMined.TxIDChainHash())

		_, err = store.Create(store.ctx, txUnMined, currentBlockHeight)
		require.NoError(t, err)

		exists, err := store.indexExists("unminedSinceIndex")
		require.NoError(t, err)
		assert.True(t, exists)

		stmt := aerospike.NewStatement(store.namespace, store.setName)

		err = stmt.SetFilter(aerospike.NewRangeFilter(fields.UnminedSince.String(), int64(currentBlockHeight), int64(4294967295)))
		require.NoError(t, err)

		// set the tx as mined
		blockIDsMap, err := store.SetMinedMulti(store.ctx, []*chainhash.Hash{txUnMined.TxIDChainHash()}, utxo.MinedBlockInfo{
			BlockID:        1,
			BlockHeight:    currentBlockHeight,
			SubtreeIdx:     1,
			OnLongestChain: false,
		})
		require.NoError(t, err)
		require.Len(t, blockIDsMap, 1)
		require.Len(t, blockIDsMap[*txUnMined.TxIDChainHash()], 1)
		require.Equal(t, []uint32{1}, blockIDsMap[*txUnMined.TxIDChainHash()])

		recordset, err := client.Query(nil, stmt)
		require.NoError(t, err)

		count := 0

		for rec := range recordset.Records() {
			assert.NotNil(t, rec.Bins[fields.UnminedSince.String()])
			assert.Equal(t, int(currentBlockHeight), rec.Bins[fields.UnminedSince.String()])
			assert.Equal(t, txUnMined.TxIDChainHash().CloneBytes(), rec.Bins[fields.TxID.String()])

			count++
		}

		assert.Equal(t, 1, count)

		// set the tx as mined
		blockIDsMap, err = store.SetMinedMulti(store.ctx, []*chainhash.Hash{txUnMined.TxIDChainHash()}, utxo.MinedBlockInfo{
			BlockID:        2,
			BlockHeight:    currentBlockHeight,
			SubtreeIdx:     2,
			OnLongestChain: true,
		})
		require.NoError(t, err)
		require.Len(t, blockIDsMap, 1)
		require.Len(t, blockIDsMap[*txUnMined.TxIDChainHash()], 2)
		require.Equal(t, []uint32{1, 2}, blockIDsMap[*txUnMined.TxIDChainHash()])

		recordset, err = client.Query(nil, stmt)
		require.NoError(t, err)

		count = 0

		for range recordset.Records() {
			count++
		}

		assert.Equal(t, 0, count)

		// set the tx as mined again, but on a fork, should change nothing
		blockIDsMap, err = store.SetMinedMulti(store.ctx, []*chainhash.Hash{txUnMined.TxIDChainHash()}, utxo.MinedBlockInfo{
			BlockID:     3,
			BlockHeight: currentBlockHeight,
			SubtreeIdx:  1,
		})
		require.NoError(t, err)
		require.Len(t, blockIDsMap, 1)
		require.Len(t, blockIDsMap[*txUnMined.TxIDChainHash()], 3)
		require.Equal(t, []uint32{1, 2, 3}, blockIDsMap[*txUnMined.TxIDChainHash()])

		recordset, err = client.Query(nil, stmt)
		require.NoError(t, err)

		count = 0

		for range recordset.Records() {
			count++
		}

		assert.Equal(t, 0, count)
	})
}

func TestLargeTxStoresExternally(t *testing.T) {
	logger := ulogger.NewErrorTestLogger(t)
	ctx := context.Background()

	tSettings := test.CreateBaseTestSettings(t)

	container, err := aeroTest.RunContainer(ctx)
	require.NoError(t, err)

	t.Cleanup(func() {
		err = container.Terminate(ctx)
		require.NoError(t, err)
	})

	host, err := container.Host(ctx)
	require.NoError(t, err)

	port, err := container.ServicePort(ctx)
	require.NoError(t, err)

	aeroURL, err := url.Parse(fmt.Sprintf("aerospike://%s:%d/test?set=utxo&externalStore=file://./data/external?hashPrefix=2", host, port))
	require.NoError(t, err)

	store, err := New(ctx, logger, tSettings, aeroURL)
	require.NoError(t, err)

	b, err := os.ReadFile("test/01d29b3fd5f2629c3b6586790312ee4a16039d8033e35a6ad0dcfa0235a39400")
	require.NoError(t, err)

	tx, err := bt.NewTxFromBytes(b)
	require.NoError(t, err)

	// Remove the external store
	err = os.RemoveAll("./data/external")
	require.NoError(t, err)

	_, err = store.Create(context.Background(), tx, 1)
	require.NoError(t, err)

	// check that the tx is stored externally
	_, err = os.Stat("./data/external/01/01d29b3fd5f2629c3b6586790312ee4a16039d8033e35a6ad0dcfa0235a39400.tx")
	require.NoError(t, err)

	// Create a transaction that spends the 2 outputs of tx
	spendTx := bt.NewTx()

	err = spendTx.FromUTXOs(&bt.UTXO{
		TxIDHash:      tx.TxIDChainHash(),
		Vout:          0,
		Satoshis:      tx.Outputs[0].Satoshis,
		LockingScript: tx.Outputs[0].LockingScript,
	}, &bt.UTXO{
		TxIDHash:      tx.TxIDChainHash(),
		Vout:          1,
		Satoshis:      tx.Outputs[1].Satoshis,
		LockingScript: tx.Outputs[1].LockingScript,
	})
	require.NoError(t, err)

	// Now let's spend the outputs
	_, err = store.Spend(context.Background(), spendTx)
	require.NoError(t, err)

	// check that the tx is stored externally
	_, err = os.Stat("./data/external/01/01d29b3fd5f2629c3b6586790312ee4a16039d8033e35a6ad0dcfa0235a39400.tx.dah")
	require.Error(t, err) // DAH should not exist

	err = store.SetBlockHeight(100_000)
	require.NoError(t, err)

	// Now mark as mined
	blockIDsMap, err := store.SetMinedMulti(context.Background(), []*chainhash.Hash{tx.TxIDChainHash()}, utxo.MinedBlockInfo{
		BlockID:     1,
		BlockHeight: 1,
		SubtreeIdx:  1,
	})
	require.NoError(t, err)
	require.Len(t, blockIDsMap, 1)
	require.Len(t, blockIDsMap[*tx.TxIDChainHash()], 1)
	require.Equal(t, []uint32{1}, blockIDsMap[*tx.TxIDChainHash()])

	dah, err := os.ReadFile("./data/external/01/01d29b3fd5f2629c3b6586790312ee4a16039d8033e35a6ad0dcfa0235a39400.tx.dah")
	require.NoError(t, err)

	require.Equal(t, "100011", string(dah))
}

// TestStore_SimpleGetters tests simple getter methods that don't require Aerospike connection
func TestStore_SimpleGetters(t *testing.T) {
	// Create a basic store instance for testing getters
	store := &Store{
		namespace:     "test-namespace",
		setName:       "test-set",
		utxoBatchSize: 25000,
	}

	t.Run("GetNamespace", func(t *testing.T) {
		assert.Equal(t, "test-namespace", store.GetNamespace())
	})

	t.Run("GetSet", func(t *testing.T) {
		assert.Equal(t, "test-set", store.GetSet())
	})

	t.Run("GetClient", func(t *testing.T) {
		// Should return nil for uninitialized client
		assert.Nil(t, store.GetClient())

		// Set a mock client and test
		mockClient := &uaerospike.Client{}
		store.client = mockClient
		assert.Equal(t, mockClient, store.GetClient())
	})
}

// TestStore_SetLogger tests the SetLogger method
func TestStore_SetLogger(t *testing.T) {
	store := &Store{}
	logger := ulogger.NewErrorTestLogger(t)

	// Should not panic
	store.SetLogger(logger)
	assert.Equal(t, logger, store.logger)
}

// TestStore_BlockHeight tests block height methods
func TestStore_BlockHeight(t *testing.T) {
	t.Run("InitialBlockHeight", func(t *testing.T) {
		store := &Store{}
		// Initial block height should be 0
		assert.Equal(t, uint32(0), store.GetBlockHeight())
	})

	t.Run("SetBlockHeightZeroError", func(t *testing.T) {
		store := &Store{}
		// SetBlockHeight with 0 should return error
		err := store.SetBlockHeight(0)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "block height cannot be zero")
	})

	t.Run("GetBlockHeightOnly", func(t *testing.T) {
		store := &Store{}

		// Test direct manipulation of atomic value for GetBlockHeight
		store.blockHeight.Store(12345)
		assert.Equal(t, uint32(12345), store.GetBlockHeight())

		store.blockHeight.Store(99999)
		assert.Equal(t, uint32(99999), store.GetBlockHeight())
	})

	// Note: SetBlockHeight requires logger and externalStore to be initialized
	// Full testing would require proper Store setup via New() function
}

// TestStore_MedianBlockTime tests median block time methods
func TestStore_MedianBlockTime(t *testing.T) {
	logger := ulogger.NewVerboseTestLogger(t)
	store := &Store{
		logger: logger,
	}

	t.Run("InitialMedianTime", func(t *testing.T) {
		// Initial median time should be 0
		assert.Equal(t, uint32(0), store.GetMedianBlockTime())
	})

	t.Run("SetAndGetMedianTime", func(t *testing.T) {
		// Test setting median block time
		testTime := uint32(1640995200) // Example timestamp
		err := store.SetMedianBlockTime(testTime)
		assert.NoError(t, err)

		// Test getting the set value
		retrievedTime := store.GetMedianBlockTime()
		assert.Equal(t, testTime, retrievedTime)
	})

	t.Run("SetZeroMedianTime", func(t *testing.T) {
		// Test setting zero (should be allowed unlike SetBlockHeight)
		err := store.SetMedianBlockTime(0)
		assert.NoError(t, err)

		retrievedTime := store.GetMedianBlockTime()
		assert.Equal(t, uint32(0), retrievedTime)
	})

	t.Run("SetMaxMedianTime", func(t *testing.T) {
		// Test setting maximum value
		maxTime := uint32(4294967295) // Max uint32
		err := store.SetMedianBlockTime(maxTime)
		assert.NoError(t, err)

		retrievedTime := store.GetMedianBlockTime()
		assert.Equal(t, maxTime, retrievedTime)
	})

	t.Run("DirectAtomicManipulation", func(t *testing.T) {
		// Test direct manipulation of atomic value for GetMedianBlockTime
		store.medianBlockTime.Store(54321)
		assert.Equal(t, uint32(54321), store.GetMedianBlockTime())

		store.medianBlockTime.Store(98765)
		assert.Equal(t, uint32(98765), store.GetMedianBlockTime())
	})
}

// TestStore_calculateOffsetForOutput tests the offset calculation method
func TestStore_calculateOffsetForOutput(t *testing.T) {
	store := &Store{utxoBatchSize: 20000}

	testCases := []struct {
		vout           uint32
		expectedOffset uint32
		description    string
	}{
		{0, 0, "First output in first batch"},
		{10000, 10000, "Middle of first batch"},
		{19999, 19999, "Last output in first batch"},
		{20000, 0, "First output in second batch"},
		{30000, 10000, "Middle of second batch"},
		{39999, 19999, "Last output in second batch"},
		{40000, 0, "First output in third batch"},
		{50000, 10000, "Middle of third batch"},
		{100000, 0, "Output in sixth batch"},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			offset := store.calculateOffsetForOutput(tc.vout)
			assert.Equal(t, tc.expectedOffset, offset, "Vout: %d", tc.vout)
		})
	}
}

// TestStore_calculateOffsetForOutput_DifferentBatchSizes tests offset calculation with different batch sizes
func TestStore_calculateOffsetForOutput_DifferentBatchSizes(t *testing.T) {
	testCases := []struct {
		batchSize      int
		vout           uint32
		expectedOffset uint32
	}{
		{10000, 0, 0},
		{10000, 5000, 5000},
		{10000, 10000, 0},
		{10000, 15000, 5000},
		{30000, 0, 0},
		{30000, 25000, 25000},
		{30000, 30000, 0},
		{30000, 35000, 5000},
		{1, 0, 0},
		{1, 5, 0},
		{1, 10, 0},
	}

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("BatchSize_%d_Vout_%d", tc.batchSize, tc.vout), func(t *testing.T) {
			store := &Store{utxoBatchSize: tc.batchSize}
			offset := store.calculateOffsetForOutput(tc.vout)
			assert.Equal(t, tc.expectedOffset, offset)
		})
	}
}

// TestStore_calculateOffsetForOutput_ErrorCases tests error cases in offset calculation
func TestStore_calculateOffsetForOutput_ErrorCases(t *testing.T) {
	logger := ulogger.NewVerboseTestLogger(t)

	t.Run("ZeroBatchSize", func(t *testing.T) {
		store := &Store{
			utxoBatchSize: 0,
			logger:        logger,
		}

		offset := store.calculateOffsetForOutput(100)
		assert.Equal(t, uint32(0), offset, "Should return 0 for zero batch size")
	})

	t.Run("NegativeBatchSize", func(t *testing.T) {
		store := &Store{
			utxoBatchSize: -1,
			logger:        logger,
		}

		offset := store.calculateOffsetForOutput(100)
		assert.Equal(t, uint32(0), offset, "Should return 0 for negative batch size")
	})

	t.Run("BatchSizeExceedsUint32Max", func(t *testing.T) {
		store := &Store{
			utxoBatchSize: int(uint64(1) << 33), // Exceeds uint32 max
			logger:        logger,
		}

		offset := store.calculateOffsetForOutput(100)
		assert.Equal(t, uint32(0), offset, "Should return 0 when batch size exceeds uint32 max")
	})
}

// Note: TestStore_Health is not included because the Health method requires
// a live Aerospike client connection and is more suitable for integration testing

// TestStore_AtomicOperations tests thread-safety of atomic operations
func TestStore_AtomicOperations(t *testing.T) {
	store := &Store{}

	t.Run("ConcurrentBlockHeightReadOperations", func(t *testing.T) {
		const numGoroutines = 50
		const numOperations = 100

		// Set initial value
		store.blockHeight.Store(1000)

		// Test concurrent reads and atomic writes
		for i := 0; i < numGoroutines; i++ {
			go func(id int) {
				for j := 0; j < numOperations; j++ {
					// Concurrent reads
					height := store.GetBlockHeight()
					assert.NotNil(t, height) // Just ensure it doesn't panic

					// Atomic store operation
					store.blockHeight.Store(uint32(id*1000 + j))
				}
			}(i)
		}

		// Allow goroutines to complete
		time.Sleep(50 * time.Millisecond)

		// Final value should be readable without issues
		finalHeight := store.GetBlockHeight()
		assert.NotEqual(t, uint32(0), finalHeight) // Should be non-zero after operations
	})

	t.Run("ConcurrentMedianBlockTimeReadOperations", func(t *testing.T) {
		const numGoroutines = 50
		const numOperations = 100

		// Set initial value
		store.medianBlockTime.Store(2000)

		// Test concurrent reads and atomic writes
		for i := 0; i < numGoroutines; i++ {
			go func(id int) {
				for j := 0; j < numOperations; j++ {
					// Concurrent reads
					medianTime := store.GetMedianBlockTime()
					assert.NotNil(t, medianTime) // Just ensure it doesn't panic

					// Atomic store operation
					store.medianBlockTime.Store(uint32(id*2000 + j))
				}
			}(i)
		}

		// Allow goroutines to complete
		time.Sleep(50 * time.Millisecond)

		// Final value should be readable without issues
		finalTime := store.GetMedianBlockTime()
		assert.NotEqual(t, uint32(0), finalTime) // Should be non-zero after operations
	})
}

// TestStore_EdgeCases tests edge cases and boundary conditions
// Note: Removed TestStore_EdgeCases as it was duplicating TestStore_calculateOffsetForOutput_ErrorCases
// and had incorrect expectations about panic behavior

// TestStore_Initialization tests Store initialization edge cases
func TestStore_Initialization(t *testing.T) {
	t.Run("ZeroValueStore", func(t *testing.T) {
		store := &Store{}

		// Test zero-value store behavior
		assert.Equal(t, "", store.GetNamespace())
		assert.Equal(t, "", store.GetSet())
		assert.Equal(t, uint32(0), store.GetBlockHeight())
		assert.Equal(t, uint32(0), store.GetMedianBlockTime())
		assert.Nil(t, store.GetClient())

		// SetLogger should not panic
		assert.NotPanics(t, func() {
			store.SetLogger(nil)
		})
	})

	t.Run("PartiallyInitializedStore", func(t *testing.T) {
		store := &Store{
			namespace:     "partial",
			setName:       "",
			utxoBatchSize: 15000,
		}

		assert.Equal(t, "partial", store.GetNamespace())
		assert.Equal(t, "", store.GetSet())

		// calculateOffsetForOutput should work with initialized batch size
		offset := store.calculateOffsetForOutput(20000)
		expectedOffset := uint32(5000) // 20000 % 15000 = 5000
		assert.Equal(t, expectedOffset, offset)
	})
}

// TestStore_indexExists_ErrorCases tests indexExists method error cases
// Note: TestStore_indexExists_ErrorCases removed because indexExists method
// requires a properly initialized Aerospike client and is better suited for integration tests

// Note: TestNew_URLParsing removed because the New function requires proper settings
// and Aerospike client setup, making it unsuitable for unit testing without integration setup

// TestStore_Constants tests package constants and variables
func TestStore_Constants(t *testing.T) {
	t.Run("MaxTxSizeInStoreInBytes", func(t *testing.T) {
		assert.Equal(t, 32*1024, MaxTxSizeInStoreInBytes)
		assert.Equal(t, 32768, MaxTxSizeInStoreInBytes)
	})

	t.Run("binNames slice", func(t *testing.T) {
		// Test that binNames contains expected fields
		expectedFields := []fields.FieldName{
			fields.Locked,
			fields.Fee,
			fields.SizeInBytes,
			fields.LockTime,
			fields.Utxos,
			fields.TxInpoints,
			fields.BlockIDs,
			fields.UtxoSpendableIn,
			fields.Conflicting,
		}

		assert.Len(t, binNames, len(expectedFields))

		// Check that all expected fields are present
		for _, expected := range expectedFields {
			assert.Contains(t, binNames, expected)
		}
	})
}

// TestStore_InterfaceCompliance tests that Store implements required interfaces
func TestStore_InterfaceCompliance(t *testing.T) {
	t.Run("Store implements utxo.Store", func(t *testing.T) {
		// This test ensures Store implements the utxo.Store interface
		var _ utxo.Store = (*Store)(nil)

		// If this compiles, the interface is implemented correctly
		assert.True(t, true, "Store successfully implements utxo.Store interface")
	})
}

// TestStore_FieldValidation tests field validation and edge cases
func TestStore_FieldValidation(t *testing.T) {
	t.Run("Empty namespace and set", func(t *testing.T) {
		store := &Store{
			namespace: "",
			setName:   "",
		}

		assert.Equal(t, "", store.GetNamespace())
		assert.Equal(t, "", store.GetSet())
	})

	t.Run("Special characters in namespace and set", func(t *testing.T) {
		store := &Store{
			namespace: "test-namespace_123",
			setName:   "test-set.456",
		}

		assert.Equal(t, "test-namespace_123", store.GetNamespace())
		assert.Equal(t, "test-set.456", store.GetSet())
	})
}

// TestStore_BatchSizeEdgeCases tests batch size calculations with edge cases
func TestStore_BatchSizeEdgeCases(t *testing.T) {
	testCases := []struct {
		name        string
		batchSize   int
		vout        uint32
		expectPanic bool
		description string
	}{
		{"Zero batch size", 0, 100, true, "Division by zero should be handled"},
		{"Negative batch size", -1, 100, true, "Negative batch size should be handled"},
		{"Very large batch size", 1000000, 500, false, "Large batch size should work"},
		{"Batch size of 1", 1, 100, false, "Minimum batch size should work"},
		{"Normal batch size", 20000, 50000, false, "Normal operations should work"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			store := &Store{utxoBatchSize: tc.batchSize}

			if tc.expectPanic {
				// For edge cases that might panic, we test they don't crash unexpectedly
				assert.NotPanics(t, func() {
					defer func() {
						if r := recover(); r != nil {
							// Panic occurred, which might be expected for edge cases
							t.Logf("Function panicked as expected for %s: %v", tc.description, r)
						}
					}()
					_ = store.calculateOffsetForOutput(tc.vout)
				})
			} else {
				assert.NotPanics(t, func() {
					offset := store.calculateOffsetForOutput(tc.vout)
					_ = offset // Use the result
				})
			}
		})
	}
}

// TestStore_ConcurrentOperations tests thread safety of atomic operations
func TestStore_ConcurrentOperations(t *testing.T) {
	logger := ulogger.NewVerboseTestLogger(t)
	store := &Store{
		logger: logger,
	}

	const numGoroutines = 10
	const numOperations = 20

	t.Run("Concurrent GetBlockHeight operations", func(t *testing.T) {
		var wg sync.WaitGroup
		wg.Add(numGoroutines)

		// Test concurrent reads of block height (safe operation)
		for i := 0; i < numGoroutines; i++ {
			go func() {
				defer wg.Done()
				for j := 0; j < numOperations; j++ {
					// Read the height (should always be safe)
					readHeight := store.GetBlockHeight()
					assert.GreaterOrEqual(t, readHeight, uint32(0))
				}
			}()
		}

		wg.Wait()
	})

	t.Run("Concurrent GetMedianBlockTime operations", func(t *testing.T) {
		var wg sync.WaitGroup
		wg.Add(numGoroutines)

		// Test concurrent reads of median block time (safe operation)
		for i := 0; i < numGoroutines; i++ {
			go func() {
				defer wg.Done()
				for j := 0; j < numOperations; j++ {
					// Read the time (should always be safe)
					readTime := store.GetMedianBlockTime()
					assert.GreaterOrEqual(t, readTime, uint32(0))
				}
			}()
		}

		wg.Wait()
	})

	t.Run("Concurrent calculateOffsetForOutput operations", func(t *testing.T) {
		store := &Store{utxoBatchSize: 20000}
		var wg sync.WaitGroup
		wg.Add(numGoroutines)

		// Test concurrent offset calculations (should be thread-safe)
		for i := 0; i < numGoroutines; i++ {
			go func(id int) {
				defer wg.Done()
				for j := 0; j < numOperations; j++ {
					vout := uint32(id*100 + j)
					offset := store.calculateOffsetForOutput(vout)
					assert.GreaterOrEqual(t, offset, uint32(0))
				}
			}(i)
		}

		wg.Wait()
	})
}
