// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2

package chain

import (
	"bytes"
	"context"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	"dappco.re/go/core/blockchain/config"
	"dappco.re/go/core/blockchain/rpc"
	"dappco.re/go/core/blockchain/types"
	"dappco.re/go/core/blockchain/wire"

	store "dappco.re/go/core/store"
)

// makeGenesisBlockBlob creates a minimal genesis block and returns its hex blob and hash.
func makeGenesisBlockBlob() (hexBlob string, hash types.Hash) {
	blk := types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 1,
			Nonce:        101011010221,
			Timestamp:    1770897600,
		},
		MinerTx: types.Transaction{
			Version: 1,
			Vin:     []types.TxInput{types.TxInputGenesis{Height: 0}},
			Vout: []types.TxOutput{
				types.TxOutputBare{
					Amount: 1000000000000,
					Target: types.TxOutToKey{Key: types.PublicKey{0x01}},
				},
			},
			Extra:      wire.EncodeVarint(0),
			Attachment: wire.EncodeVarint(0),
		},
	}
	var buf bytes.Buffer
	enc := wire.NewEncoder(&buf)
	wire.EncodeBlock(enc, &blk)
	hexBlob = hex.EncodeToString(buf.Bytes())
	hash = wire.BlockHash(&blk)
	return
}

func TestSyncOptions_Default(t *testing.T) {
	opts := DefaultSyncOptions()
	assert.False(t, opts.VerifySignatures)
	assert.Equal(t, config.MainnetForks, opts.Forks)
}

func TestSync_Good_SingleBlock(t *testing.T) {
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	// Override genesis hash for this test.
	orig := GenesisHash
	GenesisHash = genesisHash.String()
	t.Cleanup(func() { GenesisHash = orig })

	// Mock RPC server.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 1,
				"status": "OK",
			})
			return
		}

		// JSON-RPC dispatcher.
		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			result := map[string]any{
				"blocks": []map[string]any{{
					"height":               uint64(0),
					"timestamp":            uint64(1770897600),
					"base_reward":          uint64(1000000000000),
					"id":                   genesisHash.String(),
					"difficulty":           "1",
					"type":                 uint64(1),
					"blob":                 genesisBlob,
					"transactions_details": []any{},
				}},
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)

	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	h, _ := c.Height()
	if h != 1 {
		t.Errorf("height after sync: got %d, want 1", h)
	}

	blk, meta, err := c.GetBlockByHeight(0)
	if err != nil {
		t.Fatalf("GetBlockByHeight(0): %v", err)
	}
	if blk.MajorVersion != 1 {
		t.Errorf("major_version: got %d, want 1", blk.MajorVersion)
	}
	if meta.Hash != genesisHash {
		t.Errorf("hash: got %s, want %s", meta.Hash, genesisHash)
	}
}

func TestSync_Good_TwoBlocks_WithRegularTx(t *testing.T) {
	// --- Build genesis block (block 0) ---
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	// --- Build regular transaction for block 1 ---
	regularTx := types.Transaction{
		Version: 1,
		Vin: []types.TxInput{
			types.TxInputToKey{
				Amount: 1000000000000,
				KeyOffsets: []types.TxOutRef{{
					Tag:         types.RefTypeGlobalIndex,
					GlobalIndex: 0,
				}},
				KeyImage:   types.KeyImage{0xaa, 0xbb, 0xcc},
				EtcDetails: wire.EncodeVarint(0),
			},
		},
		Vout: []types.TxOutput{
			types.TxOutputBare{
				Amount: 900000000000,
				Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
			},
		},
		Extra:      wire.EncodeVarint(0),
		Attachment: wire.EncodeVarint(0),
	}

	// Wire-encode the regular tx to get its blob and hash.
	var txBuf bytes.Buffer
	txEnc := wire.NewEncoder(&txBuf)
	wire.EncodeTransaction(txEnc, &regularTx)
	regularTxBlob := hex.EncodeToString(txBuf.Bytes())
	regularTxHash := wire.TransactionHash(&regularTx)

	// --- Build block 1 ---
	minerTx1 := testCoinbaseTx(1)
	block1 := types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 1,
			Nonce:        42,
			PrevID:       genesisHash,
			Timestamp:    1770897720,
		},
		MinerTx:  minerTx1,
		TxHashes: []types.Hash{regularTxHash},
	}

	var blk1Buf bytes.Buffer
	blk1Enc := wire.NewEncoder(&blk1Buf)
	wire.EncodeBlock(blk1Enc, &block1)
	block1Blob := hex.EncodeToString(blk1Buf.Bytes())
	block1Hash := wire.BlockHash(&block1)

	// Override genesis hash for this test.
	orig := GenesisHash
	GenesisHash = genesisHash.String()
	t.Cleanup(func() { GenesisHash = orig })

	// Track which batch the server has served (to handle the sync loop).
	callCount := 0

	// Mock RPC server returning 2 blocks.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 2,
				"status": "OK",
			})
			return
		}

		// JSON-RPC dispatcher.
		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			var params struct {
				HeightStart uint64 `json:"height_start"`
				Count       uint64 `json:"count"`
			}
			json.Unmarshal(req.Params, &params)

			var blocks []map[string]any

			// Return blocks based on the requested start height.
			if params.HeightStart == 0 {
				callCount++
				blocks = []map[string]any{
					{
						"height":               uint64(0),
						"timestamp":            uint64(1770897600),
						"base_reward":          uint64(1000000000000),
						"id":                   genesisHash.String(),
						"difficulty":           "1",
						"type":                 uint64(1),
						"blob":                 genesisBlob,
						"transactions_details": []any{},
					},
					{
						"height":      uint64(1),
						"timestamp":   uint64(1770897720),
						"base_reward": uint64(1000000),
						"id":          block1Hash.String(),
						"difficulty":  "100",
						"type":        uint64(1),
						"blob":        block1Blob,
						"transactions_details": []map[string]any{
							{
								"id":   regularTxHash.String(),
								"blob": regularTxBlob,
								"fee":  uint64(100000000000),
							},
						},
					},
				}
			}

			result := map[string]any{
				"blocks": blocks,
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)

	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Verify height is 2.
	h, _ := c.Height()
	if h != 2 {
		t.Errorf("height after sync: got %d, want 2", h)
	}

	// Verify block 1 can be retrieved by height.
	gotBlk, gotMeta, err := c.GetBlockByHeight(1)
	if err != nil {
		t.Fatalf("GetBlockByHeight(1): %v", err)
	}
	if gotBlk.MajorVersion != 1 {
		t.Errorf("block 1 major_version: got %d, want 1", gotBlk.MajorVersion)
	}
	if gotMeta.Hash != block1Hash {
		t.Errorf("block 1 hash: got %s, want %s", gotMeta.Hash, block1Hash)
	}
	if gotMeta.CumulativeDiff != 101 { // genesis(1) + block1(100)
		t.Errorf("block 1 cumulative_diff: got %d, want 101", gotMeta.CumulativeDiff)
	}

	// Verify block 1 can be retrieved by hash.
	_, gotMeta2, err := c.GetBlockByHash(block1Hash)
	if err != nil {
		t.Fatalf("GetBlockByHash(block1): %v", err)
	}
	if gotMeta2.Height != 1 {
		t.Errorf("block 1 height from hash lookup: got %d, want 1", gotMeta2.Height)
	}

	// Verify the regular transaction was stored.
	gotTx, gotTxMeta, err := c.GetTransaction(regularTxHash)
	if err != nil {
		t.Fatalf("GetTransaction(regularTx): %v", err)
	}
	if gotTx.Version != 1 {
		t.Errorf("regular tx version: got %d, want 1", gotTx.Version)
	}
	if gotTxMeta.KeeperBlock != 1 {
		t.Errorf("regular tx keeper_block: got %d, want 1", gotTxMeta.KeeperBlock)
	}
	if !c.HasTransaction(regularTxHash) {
		t.Error("HasTransaction(regularTx): got false, want true")
	}

	// Verify the key image was marked as spent.
	ki := types.KeyImage{0xaa, 0xbb, 0xcc}
	spent, err := c.IsSpent(ki)
	if err != nil {
		t.Fatalf("IsSpent: %v", err)
	}
	if !spent {
		t.Error("IsSpent(key_image): got false, want true")
	}

	// Verify output indexing: genesis miner tx output + block 1 miner tx output
	// + regular tx output.
	// Genesis miner tx has 1 output at amount 1000000000000.
	// Regular tx has 1 output at amount 900000000000.
	count, err := c.OutputCount(1000000000000)
	if err != nil {
		t.Fatalf("OutputCount(1000000000000): %v", err)
	}
	if count != 1 { // only genesis miner tx
		t.Errorf("output count for 1000000000000: got %d, want 1", count)
	}

	count, err = c.OutputCount(900000000000)
	if err != nil {
		t.Fatalf("OutputCount(900000000000): %v", err)
	}
	if count != 1 { // regular tx output
		t.Errorf("output count for 900000000000: got %d, want 1", count)
	}

	// Verify top block is block 1.
	_, topMeta, err := c.TopBlock()
	if err != nil {
		t.Fatalf("TopBlock: %v", err)
	}
	if topMeta.Height != 1 {
		t.Errorf("top block height: got %d, want 1", topMeta.Height)
	}
}

func TestSync_Good_AlreadySynced(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]any{
			"height": 0,
			"status": "OK",
		})
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err != nil {
		t.Fatalf("Sync on empty: %v", err)
	}

	h, _ := c.Height()
	if h != 0 {
		t.Errorf("height: got %d, want 0", h)
	}
}

func TestSync_Bad_GetHeightError(t *testing.T) {
	// Server that returns HTTP 500 on /getheight.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err == nil {
		t.Fatal("Sync: expected error from bad getheight, got nil")
	}
}

func TestSync_Bad_FetchBlocksError(t *testing.T) {
	// Server that succeeds on /getheight but fails on JSON-RPC.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 1,
				"status": "OK",
			})
			return
		}

		// Return a JSON-RPC error for get_blocks_details.
		json.NewEncoder(w).Encode(map[string]any{
			"jsonrpc": "2.0",
			"id":      "0",
			"error": map[string]any{
				"code":    -1,
				"message": "internal error",
			},
		})
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err == nil {
		t.Fatal("Sync: expected error from bad get_blocks_details, got nil")
	}
}

func TestSync_Bad_GenesisHashMismatch(t *testing.T) {
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	// Deliberately do NOT override GenesisHash, so it mismatches.
	// The default GenesisHash is a different value.

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 1,
				"status": "OK",
			})
			return
		}

		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			result := map[string]any{
				"blocks": []map[string]any{{
					"height":               uint64(0),
					"timestamp":            uint64(1770897600),
					"base_reward":          uint64(1000000000000),
					"id":                   genesisHash.String(),
					"difficulty":           "1",
					"type":                 uint64(1),
					"blob":                 genesisBlob,
					"transactions_details": []any{},
				}},
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err == nil {
		t.Fatal("Sync: expected genesis hash mismatch error, got nil")
	}
}

func TestSync_Bad_BlockHashMismatch(t *testing.T) {
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	// Set genesis hash to a value that differs from the actual computed hash.
	wrongHash := "0000000000000000000000000000000000000000000000000000000000000001"
	orig := GenesisHash
	GenesisHash = wrongHash
	t.Cleanup(func() { GenesisHash = orig })

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 1,
				"status": "OK",
			})
			return
		}

		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			result := map[string]any{
				"blocks": []map[string]any{{
					"height":               uint64(0),
					"timestamp":            uint64(1770897600),
					"base_reward":          uint64(0),
					"id":                   wrongHash, // wrong: doesn't match computed hash
					"difficulty":           "1",
					"type":                 uint64(1),
					"blob":                 genesisBlob,
					"transactions_details": []any{},
				}},
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err == nil {
		t.Fatal("Sync: expected block hash mismatch error, got nil")
	}

	// Verify the real genesis hash is unaffected.
	_ = genesisHash
}

func TestSync_Bad_InvalidRegularTxBlob(t *testing.T) {
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	// Build block 1 with a tx hash but the RPC will return a bad tx blob.
	fakeTxHash := types.Hash{0xde, 0xad}
	minerTx1 := testCoinbaseTx(1)
	block1 := types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 1,
			Nonce:        42,
			PrevID:       genesisHash,
			Timestamp:    1770897720,
		},
		MinerTx:  minerTx1,
		TxHashes: []types.Hash{fakeTxHash},
	}

	var blk1Buf bytes.Buffer
	blk1Enc := wire.NewEncoder(&blk1Buf)
	wire.EncodeBlock(blk1Enc, &block1)
	block1Blob := hex.EncodeToString(blk1Buf.Bytes())
	block1Hash := wire.BlockHash(&block1)

	orig := GenesisHash
	GenesisHash = genesisHash.String()
	t.Cleanup(func() { GenesisHash = orig })

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 2,
				"status": "OK",
			})
			return
		}

		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			result := map[string]any{
				"blocks": []map[string]any{
					{
						"height":               uint64(0),
						"timestamp":            uint64(1770897600),
						"base_reward":          uint64(1000000000000),
						"id":                   genesisHash.String(),
						"difficulty":           "1",
						"type":                 uint64(1),
						"blob":                 genesisBlob,
						"transactions_details": []any{},
					},
					{
						"height":      uint64(1),
						"timestamp":   uint64(1770897720),
						"base_reward": uint64(1000000),
						"id":          block1Hash.String(),
						"difficulty":  "100",
						"type":        uint64(1),
						"blob":        block1Blob,
						"transactions_details": []map[string]any{
							{
								"id":   fakeTxHash.String(),
								"blob": "badc0de", // invalid: odd length hex
								"fee":  uint64(0),
							},
						},
					},
				},
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err == nil {
		t.Fatal("Sync: expected error from invalid tx blob, got nil")
	}
}

func TestSync_Bad_InvalidBlockBlob(t *testing.T) {
	// Override genesis hash to a value that matches a fake block ID.
	fakeHash := "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
	orig := GenesisHash
	GenesisHash = fakeHash
	t.Cleanup(func() { GenesisHash = orig })

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 1,
				"status": "OK",
			})
			return
		}

		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			result := map[string]any{
				"blocks": []map[string]any{{
					"height":               uint64(0),
					"timestamp":            uint64(1770897600),
					"base_reward":          uint64(0),
					"id":                   fakeHash,
					"difficulty":           "1",
					"type":                 uint64(1),
					"blob":                 "deadbeef", // invalid wire data
					"transactions_details": []any{},
				}},
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	err := c.Sync(context.Background(), client, DefaultSyncOptions())
	if err == nil {
		t.Fatal("Sync: expected error from invalid block blob, got nil")
	}
}

func TestSync_Bad_PreHardforkFreeze(t *testing.T) {
	genesisBlob, genesisHash := makeGenesisBlockBlob()
	genesisBytes, err := hex.DecodeString(genesisBlob)
	if err != nil {
		t.Fatalf("decode genesis blob: %v", err)
	}

	regularTx := types.Transaction{
		Version: 1,
		Vin: []types.TxInput{
			types.TxInputToKey{
				Amount: 1000000000000,
				KeyOffsets: []types.TxOutRef{{
					Tag:         types.RefTypeGlobalIndex,
					GlobalIndex: 0,
				}},
				KeyImage:   types.KeyImage{0xaa, 0xbb, 0xcc},
				EtcDetails: wire.EncodeVarint(0),
			},
		},
		Vout: []types.TxOutput{
			types.TxOutputBare{
				Amount: 900000000000,
				Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
			},
		},
		Extra:      wire.EncodeVarint(0),
		Attachment: wire.EncodeVarint(0),
	}

	var txBuf bytes.Buffer
	txEnc := wire.NewEncoder(&txBuf)
	wire.EncodeTransaction(txEnc, &regularTx)
	regularTxBlob := txBuf.Bytes()
	regularTxHash := wire.TransactionHash(&regularTx)

	minerTx1 := testCoinbaseTx(1)
	block1 := types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 1,
			Nonce:        42,
			PrevID:       genesisHash,
			Timestamp:    1770897720,
		},
		MinerTx:  minerTx1,
		TxHashes: []types.Hash{regularTxHash},
	}

	var blk1Buf bytes.Buffer
	blk1Enc := wire.NewEncoder(&blk1Buf)
	wire.EncodeBlock(blk1Enc, &block1)
	block1Blob := blk1Buf.Bytes()

	orig := GenesisHash
	GenesisHash = genesisHash.String()
	t.Cleanup(func() { GenesisHash = orig })

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	opts := SyncOptions{
		Forks: []config.HardFork{
			{Version: config.HF0Initial, Height: 0, Mandatory: true},
			{Version: config.HF5, Height: 2, Mandatory: true},
		},
	}

	if err := c.processBlockBlobs(genesisBytes, nil, 0, 1, opts); err != nil {
		t.Fatalf("process genesis: %v", err)
	}

	err = c.processBlockBlobs(block1Blob, [][]byte{regularTxBlob}, 1, 100, opts)
	if err == nil {
		t.Fatal("expected freeze rejection, got nil")
	}
	if !strings.Contains(err.Error(), "freeze") {
		t.Fatalf("expected freeze error, got %v", err)
	}
}

// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
func testCoinbaseTxV2(height uint64) types.Transaction {
	return types.Transaction{
		Version: types.VersionPostHF4,
		Vin:     []types.TxInput{types.TxInputGenesis{Height: height}},
		Vout: []types.TxOutput{
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{0x01},
				ConcealingPoint:  types.PublicKey{0x02},
				AmountCommitment: types.PublicKey{0x03},
				BlindedAssetID:   types.PublicKey{0x04},
				EncryptedAmount:  1000000,
				MixAttr:          0,
			},
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{0x05},
				ConcealingPoint:  types.PublicKey{0x06},
				AmountCommitment: types.PublicKey{0x07},
				BlindedAssetID:   types.PublicKey{0x08},
				EncryptedAmount:  2000000,
				MixAttr:          0,
			},
		},
		Extra:         wire.EncodeVarint(0),
		Attachment:    wire.EncodeVarint(0),
		SignaturesRaw: wire.EncodeVarint(0),
		Proofs:        wire.EncodeVarint(0),
	}
}

func TestSync_Good_ZCInputKeyImageMarkedSpent(t *testing.T) {
	// --- Build genesis block (block 0) ---
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	// --- Build a v2 transaction with a TxInputZC for block 1 ---
	zcKeyImage := types.KeyImage{0xdd, 0xee, 0xff}
	zcTx := types.Transaction{
		Version: types.VersionPostHF4,
		Vin: []types.TxInput{
			types.TxInputZC{
				KeyOffsets: []types.TxOutRef{{
					Tag:         types.RefTypeGlobalIndex,
					GlobalIndex: 0,
				}},
				KeyImage:   zcKeyImage,
				EtcDetails: wire.EncodeVarint(0),
			},
		},
		Vout: []types.TxOutput{
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{0x10},
				ConcealingPoint:  types.PublicKey{0x11},
				AmountCommitment: types.PublicKey{0x12},
				BlindedAssetID:   types.PublicKey{0x13},
				EncryptedAmount:  500000,
				MixAttr:          0,
			},
			types.TxOutputZarcanum{
				StealthAddress:   types.PublicKey{0x14},
				ConcealingPoint:  types.PublicKey{0x15},
				AmountCommitment: types.PublicKey{0x16},
				BlindedAssetID:   types.PublicKey{0x17},
				EncryptedAmount:  400000,
				MixAttr:          0,
			},
		},
		Extra:         wire.EncodeVarint(0),
		Attachment:    wire.EncodeVarint(0),
		SignaturesRaw: wire.EncodeVarint(0),
		Proofs:        wire.EncodeVarint(0),
	}

	var txBuf bytes.Buffer
	txEnc := wire.NewEncoder(&txBuf)
	wire.EncodeTransaction(txEnc, &zcTx)
	zcTxBlob := hex.EncodeToString(txBuf.Bytes())
	zcTxHash := wire.TransactionHash(&zcTx)

	// --- Build block 1 (v2 block) ---
	minerTx1 := testCoinbaseTxV2(1)
	block1 := types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 2,
			Nonce:        42,
			PrevID:       genesisHash,
			Timestamp:    1770897720,
		},
		MinerTx:  minerTx1,
		TxHashes: []types.Hash{zcTxHash},
	}

	var blk1Buf bytes.Buffer
	blk1Enc := wire.NewEncoder(&blk1Buf)
	wire.EncodeBlock(blk1Enc, &block1)
	block1Blob := hex.EncodeToString(blk1Buf.Bytes())
	block1Hash := wire.BlockHash(&block1)

	// Override genesis hash for this test.
	orig := GenesisHash
	GenesisHash = genesisHash.String()
	t.Cleanup(func() { GenesisHash = orig })

	// Mock RPC server returning 2 blocks.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 2,
				"status": "OK",
			})
			return
		}

		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			blocks := []map[string]any{
				{
					"height":               uint64(0),
					"timestamp":            uint64(1770897600),
					"base_reward":          uint64(1000000000000),
					"id":                   genesisHash.String(),
					"difficulty":           "1",
					"type":                 uint64(1),
					"blob":                 genesisBlob,
					"transactions_details": []any{},
				},
				{
					"height":      uint64(1),
					"timestamp":   uint64(1770897720),
					"base_reward": uint64(1000000),
					"id":          block1Hash.String(),
					"difficulty":  "100",
					"type":        uint64(1),
					"blob":        block1Blob,
					"transactions_details": []map[string]any{
						{
							"id":   zcTxHash.String(),
							"blob": zcTxBlob,
							"fee":  uint64(0),
						},
					},
				},
			}

			result := map[string]any{
				"blocks": blocks,
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)

	// Use custom forks where HF4 is active from height 0 so ZC inputs pass validation.
	opts := SyncOptions{
		VerifySignatures: false,
		Forks: []config.HardFork{
			{Version: config.HF1, Height: 0, Mandatory: true},
			{Version: config.HF2, Height: 0, Mandatory: true},
			{Version: config.HF3, Height: 0, Mandatory: true},
			{Version: config.HF4Zarcanum, Height: 0, Mandatory: true},
		},
	}

	err := c.Sync(context.Background(), client, opts)
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Verify height is 2.
	h, _ := c.Height()
	if h != 2 {
		t.Errorf("height after sync: got %d, want 2", h)
	}

	// Verify the ZC key image was marked as spent.
	spent, err := c.IsSpent(zcKeyImage)
	if err != nil {
		t.Fatalf("IsSpent: %v", err)
	}
	if !spent {
		t.Error("IsSpent(zc_key_image): got false, want true")
	}
}

func TestSync_Good_HTLCInputKeyImageMarkedSpent(t *testing.T) {
	genesisBlob, genesisHash := makeGenesisBlockBlob()

	htlcKeyImage := types.KeyImage{0x44, 0x55, 0x66}
	htlcTx := types.Transaction{
		Version: 1,
		Vin: []types.TxInput{
			types.TxInputHTLC{
				HTLCOrigin: "contract-1",
				Amount:     1000000000000,
				KeyOffsets: []types.TxOutRef{{
					Tag:         types.RefTypeGlobalIndex,
					GlobalIndex: 0,
				}},
				KeyImage:   htlcKeyImage,
				EtcDetails: wire.EncodeVarint(0),
			},
		},
		Vout: []types.TxOutput{
			types.TxOutputBare{
				Amount: 900000000000,
				Target: types.TxOutToKey{Key: types.PublicKey{0x21}},
			},
		},
		Extra:      wire.EncodeVarint(0),
		Attachment: wire.EncodeVarint(0),
	}

	var txBuf bytes.Buffer
	txEnc := wire.NewEncoder(&txBuf)
	wire.EncodeTransaction(txEnc, &htlcTx)
	htlcTxBlob := hex.EncodeToString(txBuf.Bytes())
	htlcTxHash := wire.TransactionHash(&htlcTx)

	minerTx1 := testCoinbaseTx(1)
	block1 := types.Block{
		BlockHeader: types.BlockHeader{
			MajorVersion: 1,
			Nonce:        42,
			PrevID:       genesisHash,
			Timestamp:    1770897720,
		},
		MinerTx:  minerTx1,
		TxHashes: []types.Hash{htlcTxHash},
	}

	var blk1Buf bytes.Buffer
	blk1Enc := wire.NewEncoder(&blk1Buf)
	wire.EncodeBlock(blk1Enc, &block1)
	block1Blob := hex.EncodeToString(blk1Buf.Bytes())
	block1Hash := wire.BlockHash(&block1)

	orig := GenesisHash
	GenesisHash = genesisHash.String()
	t.Cleanup(func() { GenesisHash = orig })

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		if r.URL.Path == "/getheight" {
			json.NewEncoder(w).Encode(map[string]any{
				"height": 2,
				"status": "OK",
			})
			return
		}

		var req struct {
			Method string          `json:"method"`
			Params json.RawMessage `json:"params"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		switch req.Method {
		case "get_blocks_details":
			blocks := []map[string]any{
				{
					"height":               uint64(0),
					"timestamp":            uint64(1770897600),
					"base_reward":          uint64(1000000000000),
					"id":                   genesisHash.String(),
					"difficulty":           "1",
					"type":                 uint64(1),
					"blob":                 genesisBlob,
					"transactions_details": []any{},
				},
				{
					"height":      uint64(1),
					"timestamp":   uint64(1770897720),
					"base_reward": uint64(1000000),
					"id":          block1Hash.String(),
					"difficulty":  "100",
					"type":        uint64(1),
					"blob":        block1Blob,
					"transactions_details": []map[string]any{
						{
							"id":   htlcTxHash.String(),
							"blob": htlcTxBlob,
							"fee":  uint64(100000000000),
						},
					},
				},
			}

			result := map[string]any{
				"blocks": blocks,
				"status": "OK",
			}
			resultBytes, _ := json.Marshal(result)
			json.NewEncoder(w).Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      "0",
				"result":  json.RawMessage(resultBytes),
			})
		}
	}))
	defer srv.Close()

	s, _ := store.New(":memory:")
	defer s.Close()
	c := New(s)

	client := rpc.NewClient(srv.URL)
	opts := SyncOptions{
		VerifySignatures: false,
		Forks: []config.HardFork{
			{Version: config.HF1, Height: 0, Mandatory: true},
			{Version: config.HF2, Height: 0, Mandatory: true},
		},
	}

	err := c.Sync(context.Background(), client, opts)
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	spent, err := c.IsSpent(htlcKeyImage)
	if err != nil {
		t.Fatalf("IsSpent: %v", err)
	}
	if !spent {
		t.Error("IsSpent(htlc_key_image): got false, want true")
	}
}
