package blockvalidation

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/bsv-blockchain/go-bt/v2/chainhash"
	"github.com/bsv-blockchain/teranode/errors"
	"github.com/bsv-blockchain/teranode/model"
	"github.com/bsv-blockchain/teranode/services/blockvalidation/catchup"
	"github.com/bsv-blockchain/teranode/services/blockvalidation/testhelpers"
	"github.com/stretchr/testify/assert"
)

// TestValidateBlockHeaderBytes tests the validateBlockHeaderBytes function
func TestValidateBlockHeaderBytes(t *testing.T) {
	t.Run("ValidHeaderBytes", func(t *testing.T) {
		// Create exactly 80 bytes (valid block header size)
		headerBytes := make([]byte, model.BlockHeaderSize)
		err := catchup.ValidateBlockHeaderBytes(headerBytes)
		assert.NoError(t, err, "Valid header bytes should pass")
	})

	t.Run("MultipleValidHeaders", func(t *testing.T) {
		// Create exactly 240 bytes (3 headers)
		headerBytes := make([]byte, model.BlockHeaderSize*3)
		err := catchup.ValidateBlockHeaderBytes(headerBytes)
		assert.NoError(t, err, "Multiple valid header bytes should pass")
	})

	t.Run("EmptyBytes", func(t *testing.T) {
		err := catchup.ValidateBlockHeaderBytes([]byte{})
		assert.NoError(t, err, "Empty bytes should be valid")
	})

	t.Run("InvalidLength", func(t *testing.T) {
		// Create 100 bytes (not divisible by 80)
		headerBytes := make([]byte, 100)
		err := catchup.ValidateBlockHeaderBytes(headerBytes)
		assert.Error(t, err, "Invalid length should fail")
		assert.Contains(t, err.Error(), "must be divisible by")
	})

	t.Run("PartialHeader", func(t *testing.T) {
		// Create 79 bytes (one byte short)
		headerBytes := make([]byte, 79)
		err := catchup.ValidateBlockHeaderBytes(headerBytes)
		assert.Error(t, err, "Partial header should fail")
		assert.Contains(t, err.Error(), "must be divisible by")
	})
}

// TestParseBlockHeaders tests the parseBlockHeaders function
func TestParseBlockHeaders(t *testing.T) {
	t.Run("ParseValidHeaders", func(t *testing.T) {
		// Create a header with valid proof of work
		// createTestHeaderAtHeight now mines the header, so it should pass PoW validation
		header := testhelpers.CreateTestHeaderAtHeight(1)
		headerBytes := header.Bytes()

		headers, err := catchup.ParseBlockHeaders(headerBytes)
		// The header should pass validation since it's properly mined
		assert.NoError(t, err, "Should have no error for valid header")
		assert.Len(t, headers, 1, "Should return one header")
		if len(headers) > 0 {
			assert.Equal(t, header.Hash(), headers[0].Hash(), "Returned header should match")
		}
	})

	t.Run("ParseEmptyBytes", func(t *testing.T) {
		headers, err := catchup.ParseBlockHeaders([]byte{})
		assert.Empty(t, headers, "Should return empty headers")
		assert.NoError(t, err, "Should have no error")
	})

	t.Run("ParseZeroHeader", func(t *testing.T) {
		// Create header bytes that are all zeros (valid size but invalid content)
		headerBytes := make([]byte, model.BlockHeaderSize)

		// This will fail proof of work validation
		headers, err := catchup.ParseBlockHeaders(headerBytes)
		assert.Empty(t, headers, "Should not return invalid headers")
		assert.NotNil(t, err, "Should have validation error")
		// Zero merkle root on non-genesis is invalid
		assert.True(t,
			strings.Contains(err.Error(), "proof of work") || strings.Contains(err.Error(), "merkle root"),
			"Should have proof of work or merkle root error")
	})

	t.Run("ParseMalformedHeader", func(t *testing.T) {
		// Create header bytes with some non-zero values but still invalid
		headerBytes := make([]byte, model.BlockHeaderSize)
		// Set version to something
		headerBytes[0] = 1

		headers, err := catchup.ParseBlockHeaders(headerBytes)
		assert.Empty(t, headers, "Should not return headers that fail validation")
		assert.NotNil(t, err, "Should have validation error")
	})
}

// TestBuildBlockLocatorString tests the buildBlockLocatorString function
func TestBuildBlockLocatorString_Helpers(t *testing.T) {
	t.Run("HandlesEmptyInput", func(t *testing.T) {
		result := catchup.BuildBlockLocatorString(nil)
		assert.Equal(t, "", result)

		result = catchup.BuildBlockLocatorString([]*chainhash.Hash{})
		assert.Equal(t, "", result)
	})

	t.Run("BuildsCorrectString", func(t *testing.T) {
		// Create test hashes
		hash1 := chainhash.HashH([]byte("test1"))
		hash2 := chainhash.HashH([]byte("test2"))
		hashes := []*chainhash.Hash{&hash1, &hash2}

		result := catchup.BuildBlockLocatorString(hashes)
		expected := hash1.String() + hash2.String()
		assert.Equal(t, expected, result)
		assert.Len(t, result, 128) // Each hash is 64 chars
	})

	t.Run("SingleHash", func(t *testing.T) {
		hash := chainhash.HashH([]byte("single"))
		hashes := []*chainhash.Hash{&hash}

		result := catchup.BuildBlockLocatorString(hashes)
		assert.Equal(t, hash.String(), result)
		assert.Len(t, result, 64) // Single hash is 64 chars
	})

	t.Run("ManyHashes", func(t *testing.T) {
		// Create 100 hashes
		hashes := make([]*chainhash.Hash, 100)
		for i := range hashes {
			hash := chainhash.HashH([]byte(fmt.Sprintf("hash%d", i)))
			hashes[i] = &hash
		}

		result := catchup.BuildBlockLocatorString(hashes)
		assert.Len(t, result, 6400) // 100 * 64 chars

		// Verify first hash is included
		assert.Contains(t, result, hashes[0].String())
	})
}

// BenchmarkBuildBlockLocatorString benchmarks the string builder optimization
func BenchmarkBuildBlockLocatorString_Helpers(b *testing.B) {
	// Create test data with various sizes
	sizes := []int{10, 100, 1000}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("Size_%d", size), func(b *testing.B) {
			hashes := make([]*chainhash.Hash, size)
			for i := range hashes {
				hash := chainhash.HashH([]byte(fmt.Sprintf("test%d", i)))
				hashes[i] = &hash
			}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_ = catchup.BuildBlockLocatorString(hashes)
			}
		})
	}
}

// TestCheckContextCancellation tests context cancellation checking
func TestCheckContextCancellation(t *testing.T) {
	t.Run("NotCancelled", func(t *testing.T) {
		ctx := context.Background()
		err := catchup.CheckContextCancellation(ctx)
		assert.NoError(t, err, "Non-cancelled context should not error")
	})

	t.Run("Cancelled", func(t *testing.T) {
		ctx, cancel := context.WithCancel(context.Background())
		cancel()

		err := catchup.CheckContextCancellation(ctx)
		assert.Error(t, err, "Cancelled context should error")
		assert.Contains(t, err.Error(), "operation cancelled")
	})

	t.Run("Timeout", func(t *testing.T) {
		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
		defer cancel()

		// Wait for timeout
		time.Sleep(2 * time.Millisecond)

		err := catchup.CheckContextCancellation(ctx)
		assert.Error(t, err, "Timed out context should error")
		assert.Contains(t, err.Error(), "operation cancelled")
	})
}

// TestFilterNewHeaders tests the filterNewHeaders function
// Note: This test requires a Server instance and is tested as part of Server tests
/*
func TestFilterNewHeaders(t *testing.T) {
	t.Run("AllNewHeaders", func(t *testing.T) {
		server, mockBlockchain, _, cleanup := setupTestCatchupServer(t)
		defer cleanup()

		ctx := context.Background()
		headers := []*model.BlockHeader{
			createTestHeaderAtHeight(1),
			createTestHeaderAtHeight(2),
			createTestHeaderAtHeight(3),
		}

		// Mock all headers as non-existent
		for _, header := range headers {
			mockBlockchain.On("GetBlockExists", ctx, header.Hash()).Return(false, nil)
		}

		newHeaders, err := server.filterNewHeaders(ctx, headers)
		assert.NoError(t, err)
		assert.Len(t, newHeaders, 3, "All headers should be new")
	})

	t.Run("SomeExistingHeaders", func(t *testing.T) {
		server, mockBlockchain, _, cleanup := setupTestCatchupServer(t)
		defer cleanup()

		ctx := context.Background()
		headers := []*model.BlockHeader{
			createTestHeaderAtHeight(1),
			createTestHeaderAtHeight(2),
			createTestHeaderAtHeight(3),
		}

		// Mock first header as existing, others as new
		mockBlockchain.On("GetBlockExists", ctx, headers[0].Hash()).Return(true, nil)
		mockBlockchain.On("GetBlockExists", ctx, headers[1].Hash()).Return(false, nil)
		mockBlockchain.On("GetBlockExists", ctx, headers[2].Hash()).Return(false, nil)

		newHeaders, err := server.filterNewHeaders(ctx, headers)
		assert.NoError(t, err)
		assert.Len(t, newHeaders, 2, "Only new headers should be returned")
		assert.Equal(t, headers[1], newHeaders[0])
		assert.Equal(t, headers[2], newHeaders[1])
	})

	t.Run("AllExistingHeaders", func(t *testing.T) {
		server, mockBlockchain, _, cleanup := setupTestCatchupServer(t)
		defer cleanup()

		ctx := context.Background()
		headers := []*model.BlockHeader{
			createTestHeaderAtHeight(1),
			createTestHeaderAtHeight(2),
		}

		// Mock all headers as existing
		for _, header := range headers {
			mockBlockchain.On("GetBlockExists", ctx, header.Hash()).Return(true, nil)
		}

		newHeaders, err := server.filterNewHeaders(ctx, headers)
		assert.NoError(t, err)
		assert.Empty(t, newHeaders, "No new headers should be returned")
	})

	t.Run("ErrorCheckingExistence", func(t *testing.T) {
		server, mockBlockchain, _, cleanup := setupTestCatchupServer(t)
		defer cleanup()

		ctx := context.Background()
		headers := []*model.BlockHeader{
			createTestHeaderAtHeight(1),
		}

		// Mock error when checking existence
		mockBlockchain.On("GetBlockExists", ctx, headers[0].Hash()).Return(
			false, errors.NewServiceError("database error"),
		)

		newHeaders, err := server.filterNewHeaders(ctx, headers)
		assert.Error(t, err)
		assert.Nil(t, newHeaders)
		assert.Contains(t, err.Error(), "failed to check block existence")
	})

	t.Run("EmptyHeaders", func(t *testing.T) {
		server, _, _, cleanup := setupTestCatchupServer(t)
		defer cleanup()

		ctx := context.Background()
		newHeaders, err := server.filterNewHeaders(ctx, []*model.BlockHeader{})
		assert.NoError(t, err)
		assert.Empty(t, newHeaders)
	})
}
*/

// TestFetchHeadersWithRetry tests the fetchHeadersWithRetry function
func TestFetchHeadersWithRetry(t *testing.T) {
	// Note: This function uses util.DoHTTPRequest which would require mocking
	// the HTTP client. For now, we'll test the error categorization logic
	// by testing the error handling paths.

	t.Run("TimeoutError", func(t *testing.T) {
		// Test that timeout errors are properly categorized
		// This would require mocking util.DoHTTPRequest
		// For unit tests, we focus on the error categorization logic
		assert.True(t, true, "Timeout error categorization tested in integration tests")
	})

	t.Run("ConnectionRefusedError", func(t *testing.T) {
		// Test that connection refused errors are properly categorized
		assert.True(t, true, "Connection refused error categorization tested in integration tests")
	})
}

// TestCreateCatchupResult tests the createCatchupResult function
func TestCreateCatchupResult(t *testing.T) {
	t.Run("SuccessfulResult", func(t *testing.T) {
		headers := []*model.BlockHeader{
			testhelpers.CreateTestHeaderAtHeight(1),
			testhelpers.CreateTestHeaderAtHeight(2),
		}
		targetHash := chainhash.HashH([]byte("target"))
		startHash := chainhash.HashH([]byte("start"))
		startTime := time.Now().Add(-5 * time.Minute)

		result := catchup.CreateCatchupResult(
			headers,
			&targetHash,
			&startHash,
			100,
			startTime,
			"http://peer:8080",
			10,
			nil,
			true,
			"",
		)

		assert.NotNil(t, result)
		assert.Len(t, result.Headers, 2)
		assert.Equal(t, &targetHash, result.TargetHash)
		assert.Equal(t, &startHash, result.StartHash)
		assert.Equal(t, uint32(100), result.StartHeight)
		assert.Equal(t, 10, result.TotalIterations)
		assert.True(t, result.ReachedTarget)
		assert.False(t, result.StoppedEarly)
		assert.Equal(t, headers[1].Hash(), result.LastProcessedHash)
		assert.True(t, result.Duration > 0)
	})

	t.Run("PartialSuccess", func(t *testing.T) {
		headers := []*model.BlockHeader{
			testhelpers.CreateTestHeaderAtHeight(1),
		}
		targetHash := chainhash.HashH([]byte("target"))
		startHash := chainhash.HashH([]byte("start"))
		startTime := time.Now().Add(-2 * time.Minute)

		errs := []catchup.IterationError{
			{Iteration: 5, Error: errors.NewServiceError("timeout")},
		}

		result := catchup.CreateCatchupResult(
			headers,
			&targetHash,
			&startHash,
			100,
			startTime,
			"http://peer:8080",
			10,
			errs,
			false,
			"Stopped due to errors",
		)

		assert.NotNil(t, result)
		assert.Len(t, result.Headers, 1)
		assert.True(t, result.PartialSuccess)
		assert.False(t, result.ReachedTarget)
		assert.True(t, result.StoppedEarly)
		assert.Equal(t, "Stopped due to errors", result.StopReason)
		assert.Len(t, result.FailedIterations, 1)
	})

	t.Run("NoHeaders", func(t *testing.T) {
		targetHash := chainhash.HashH([]byte("target"))
		startHash := chainhash.HashH([]byte("start"))
		startTime := time.Now()

		result := catchup.CreateCatchupResult(
			nil,
			&targetHash,
			&startHash,
			100,
			startTime,
			"http://peer:8080",
			1,
			nil,
			false,
			"No headers received",
		)

		assert.NotNil(t, result)
		assert.Empty(t, result.Headers)
		assert.Nil(t, result.LastProcessedHash)
		assert.False(t, result.ReachedTarget)
		assert.Equal(t, "No headers received", result.StopReason)
	})
}

// Note: setupTestCatchupServer is already defined in catchup_coinbase_maturity_test.go
// and reused across test files in this package
