using NBitcoin;
using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Crypto;
using PalladiumWallet.Core.Net;
using PalladiumWallet.Core.Wallet;
using PalladiumWallet.Tests.Net;

namespace PalladiumWallet.Tests.Wallet;

/// <summary>
/// Tests for the transaction detail assembly (§10): fee from resolved inputs,
/// mine/theirs attribution, RBF flag, coinbase, and the degraded paths when the
/// server cannot provide a previous transaction.
/// </summary>
public class TransactionInspectorTests
{
    private static readonly Network Net = PalladiumNetworks.Regtest;

    private static HdAccount Account()
    {
        Assert.True(Bip39.TryParse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
            out var mnemonic));
        return HdAccount.FromMnemonic(mnemonic!, null, ScriptKind.NativeSegwit, ChainProfiles.Regtest);
    }

    /// <summary>Funding (1M sats to receive/0) + spend (600k external, 390k change/0, 10k fee, RBF).</summary>
    private static (Transaction Funding, Transaction Spend, BitcoinAddress External, HdAccount Account) SpendPair()
    {
        var account = Account();
        var funding = Net.CreateTransaction();
        funding.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
        funding.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));

        var external = new Key().PubKey.GetAddress(ScriptPubKeyType.Segwit, Net);
        var spend = Net.CreateTransaction();
        spend.Inputs.Add(new TxIn(new OutPoint(funding, 0)) { Sequence = 0xfffffffd });
        spend.Outputs.Add(Money.Satoshis(600_000), external);
        spend.Outputs.Add(Money.Satoshis(390_000), account.GetChangeAddress(0));
        return (funding, spend, external, account);
    }

    private static async Task<(FakeElectrumServer Server, ElectrumClient Client)> StartAsync(
        params Transaction[] served)
    {
        var byId = served.ToDictionary(t => t.GetHash().ToString());
        var server = new FakeElectrumServer();
        server.Handle("blockchain.transaction.get", p =>
            byId.TryGetValue(p[0].GetString()!, out var tx)
                ? tx.ToHex()
                : throw new FakeElectrumError(-32600, "no such transaction"));
        server.Handle("blockchain.block.header", p =>
        {
            var height = p[0].GetInt32();
            var header = Net.Consensus.ConsensusFactory.CreateBlockHeader();
            header.BlockTime = DateTimeOffset.FromUnixTimeSeconds(1_700_000_000 + height);
            header.Bits = new Target(0x1d00ffffu);
            return Convert.ToHexString(header.ToBytes()).ToLowerInvariant();
        });
        var client = await ElectrumClient.ConnectAsync(server.Host, server.Port, useSsl: false);
        return (server, client);
    }

    private static HashSet<string> Owned(HdAccount account) =>
    [
        account.GetReceiveAddress(0).ToString(),
        account.GetChangeAddress(0).ToString(),
    ];

    [Fact]
    public async Task Una_spesa_confermata_riporta_fee_conferme_e_attribuzione_mine_theirs()
    {
        var (funding, spend, external, account) = SpendPair();
        var (server, client) = await StartAsync(funding, spend);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: -610_000, verified: true);

        Assert.Equal(10_000, details.FeeSats);
        Assert.Equal(1_000_000, details.TotalInSats);
        Assert.Equal(990_000, details.TotalOutSats);
        Assert.Equal(100, details.Confirmations); // 200 − 101 + 1
        Assert.True(details.RbfSignaled);
        Assert.True(details.Verified);
        Assert.False(details.IsCoinbase);
        Assert.False(details.IsIncoming);
        Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_700_000_101), details.BlockTime);
        Assert.Equal((double)10_000 / details.VirtualSize, details.FeeRateSatPerVb);

        var input = Assert.Single(details.Inputs);
        Assert.True(input.IsMine);
        Assert.Equal(1_000_000, input.AmountSats);
        Assert.Equal(account.GetReceiveAddress(0).ToString(), input.Address);

        Assert.Equal(2, details.Outputs.Count);
        Assert.Equal(600_000, details.SentToOthersSats);
        Assert.Equal(390_000, details.ReceivedSats);
        Assert.Contains(details.Outputs, o => o.IsMine && o.AmountSats == 390_000);
        Assert.Contains(details.Outputs, o => !o.IsMine && o.AmountSats == 600_000);

        // Outgoing tx: the counterparty is the external recipient.
        Assert.Equal([external.ToString()], details.CounterpartyAddresses);
    }

    [Fact]
    public async Task Una_ricezione_indica_il_mittente_come_controparte()
    {
        var account = Account();
        var senderKey = new Key();
        var senderAddr = senderKey.PubKey.GetAddress(ScriptPubKeyType.Segwit, Net);

        // The sender's coin, then the payment to us spending it.
        var senderFunding = Net.CreateTransaction();
        senderFunding.Inputs.Add(new TxIn(new OutPoint(uint256.One, 1)));
        senderFunding.Outputs.Add(Money.Satoshis(2_000_000), senderAddr);

        var payment = Net.CreateTransaction();
        payment.Inputs.Add(new TxIn(new OutPoint(senderFunding, 0)));
        payment.Outputs.Add(Money.Satoshis(1_500_000), account.GetReceiveAddress(0));

        var (server, client) = await StartAsync(senderFunding, payment);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, payment.GetHash().ToString(), tipHeight: 200, height: 150,
            Owned(account), netSats: 1_500_000, verified: true);

        Assert.True(details.IsIncoming);
        Assert.Equal([senderAddr.ToString()], details.CounterpartyAddresses);
        Assert.Equal(500_000, details.FeeSats); // 2M in − 1.5M out
    }

    [Fact]
    public async Task Una_coinbase_non_ha_fee_ne_importi_in_ingresso()
    {
        var account = Account();
        var coinbase = Net.CreateTransaction();
        coinbase.Inputs.Add(new TxIn());
        coinbase.Outputs.Add(Money.Satoshis(5_000_000_000), account.GetReceiveAddress(0));

        var (server, client) = await StartAsync(coinbase);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, coinbase.GetHash().ToString(), tipHeight: 200, height: 190,
            Owned(account), netSats: 5_000_000_000, verified: true);

        Assert.True(details.IsCoinbase);
        Assert.Null(details.FeeSats);
        Assert.Null(details.TotalInSats);
        Assert.Null(details.FeeRateSatPerVb);
        Assert.Equal(11, details.Confirmations);
        var input = Assert.Single(details.Inputs);
        Assert.True(input.IsCoinbase);
        Assert.Null(input.AmountSats);
        // No previous transactions to resolve.
        Assert.Equal(0, server.CallCount("blockchain.transaction.get") - 1); // only the coinbase itself
    }

    [Fact]
    public async Task Se_la_tx_precedente_non_e_recuperabile_la_fee_resta_ignota()
    {
        var (funding, spend, _, account) = SpendPair();
        // The server only knows the spend: the funding lookup fails.
        var (server, client) = await StartAsync(spend);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: -610_000, verified: false);

        Assert.Null(details.FeeSats);
        Assert.Null(details.TotalInSats);
        var input = Assert.Single(details.Inputs);
        Assert.Null(input.AmountSats);
        Assert.False(input.IsMine); // unresolvable → not attributable
        Assert.Equal(funding.GetHash().ToString(), input.PrevTxid);
    }

    [Fact]
    public async Task Una_tx_in_mempool_non_ha_conferme_ne_timestamp()
    {
        var (funding, spend, _, account) = SpendPair();
        var (server, client) = await StartAsync(funding, spend);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 0,
            Owned(account), netSats: -610_000, verified: false);

        Assert.Equal(0, details.Confirmations);
        Assert.Null(details.BlockTime);
        Assert.Equal(0, server.CallCount("blockchain.block.header"));
    }

    [Fact]
    public async Task Un_output_OP_RETURN_con_testo_UTF8_viene_decodificato()
    {
        var account = Account();
        var tx = Net.CreateTransaction();
        tx.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
        tx.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));
        tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey("hello palladium"u8.ToArray()));

        var (server, client) = await StartAsync(tx);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, tx.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: 1_000_000, verified: true);

        var opReturn = Assert.Single(details.Outputs, o => o.Index == 1);
        Assert.Null(opReturn.Address);
        Assert.Equal("hello palladium", opReturn.OpReturnText);
    }

    [Fact]
    public async Task Un_output_OP_RETURN_con_dati_binari_ricade_su_hex()
    {
        var account = Account();
        byte[] data = [0x00, 0x01, 0xFF, 0xFE, 0x02];
        var tx = Net.CreateTransaction();
        tx.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
        tx.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));
        tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey(data));

        var (server, client) = await StartAsync(tx);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, tx.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: 1_000_000, verified: true);

        var opReturn = Assert.Single(details.Outputs, o => o.Index == 1);
        Assert.Equal("0x" + Convert.ToHexString(data), opReturn.OpReturnText);
    }

    [Fact]
    public async Task Piu_output_OP_RETURN_nella_stessa_tx_sono_decodificati_singolarmente()
    {
        var account = Account();
        var tx = Net.CreateTransaction();
        tx.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
        tx.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));
        tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey("first"u8.ToArray()));
        tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey("second"u8.ToArray()));

        var (server, client) = await StartAsync(tx);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, tx.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: 1_000_000, verified: true);

        Assert.Equal("first", details.Outputs.Single(o => o.Index == 1).OpReturnText);
        Assert.Equal("second", details.Outputs.Single(o => o.Index == 2).OpReturnText);
    }

    [Fact]
    public async Task Un_output_normale_non_ha_testo_OP_RETURN()
    {
        var (funding, spend, _, account) = SpendPair();
        var (server, client) = await StartAsync(funding, spend);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: -610_000, verified: true);

        Assert.All(details.Outputs, o => Assert.Null(o.OpReturnText));
    }

    [Fact]
    public async Task Il_tag_pool_nello_scriptSig_della_coinbase_viene_estratto()
    {
        var account = Account();
        var coinbase = Net.CreateTransaction();
        // BIP34 height push (binary) followed by a pool tag (printable ASCII).
        var scriptSig = new Script(Op.GetPushOp(190), Op.GetPushOp("/slush/"u8.ToArray()));
        coinbase.Inputs.Add(new TxIn(new OutPoint(uint256.Zero, 0xffffffff), scriptSig));
        coinbase.Outputs.Add(Money.Satoshis(5_000_000_000), account.GetReceiveAddress(0));

        var (server, client) = await StartAsync(coinbase);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, coinbase.GetHash().ToString(), tipHeight: 200, height: 190,
            Owned(account), netSats: 5_000_000_000, verified: true);

        var input = Assert.Single(details.Inputs);
        Assert.True(input.IsCoinbase);
        Assert.Contains("/slush/", input.CoinbaseTag);
    }

    [Fact]
    public async Task Uno_scriptSig_coinbase_senza_testo_stampabile_non_produce_tag()
    {
        var account = Account();
        var coinbase = Net.CreateTransaction();
        var scriptSig = new Script(Op.GetPushOp([0x00, 0x01, 0x02]));
        coinbase.Inputs.Add(new TxIn(new OutPoint(uint256.Zero, 0xffffffff), scriptSig));
        coinbase.Outputs.Add(Money.Satoshis(5_000_000_000), account.GetReceiveAddress(0));

        var (server, client) = await StartAsync(coinbase);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, coinbase.GetHash().ToString(), tipHeight: 200, height: 190,
            Owned(account), netSats: 5_000_000_000, verified: true);

        var input = Assert.Single(details.Inputs);
        Assert.Null(input.CoinbaseTag);
    }

    [Fact]
    public async Task Un_input_normale_non_ha_tag_coinbase()
    {
        var (funding, spend, _, account) = SpendPair();
        var (server, client) = await StartAsync(funding, spend);
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 101,
            Owned(account), netSats: -610_000, verified: true);

        Assert.All(details.Inputs, i => Assert.Null(i.CoinbaseTag));
    }

    [Fact]
    public async Task La_cache_delle_tx_evita_le_richieste_al_server()
    {
        var (funding, spend, _, account) = SpendPair();
        var cache = new Dictionary<string, Transaction>
        {
            [funding.GetHash().ToString()] = funding,
            [spend.GetHash().ToString()] = spend,
        };
        var (server, client) = await StartAsync();
        await using var _ = server; await using var __ = client;

        var details = await TransactionInspector.FetchAsync(
            client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 0,
            Owned(account), netSats: -610_000, verified: false, cache);

        Assert.Equal(10_000, details.FeeSats);
        Assert.Equal(0, server.CallCount("blockchain.transaction.get"));
    }
}
