"""
End-to-end inheritance flow tests (mock, no network, no daemon).

These tests exercise the real inheritance-building and state-assignment logic
of the plugin using *plausible data*:

    * heir / will-executor / change addresses are genuine regtest addresses
      sourced read-only from the giovanna7 wallet (see ``bal_fixtures``);
    * UTXOs and txids are synthesised so the suite needs no running Electrum.

Covered scenarios (mirroring the project spec):

    1. Build an inheritance with 3 heirs + 2 will-executors and verify every
       heir's output value equals its resolved amount.
    2. Change a heir's ADDRESS, rebuild: the new will is anticipated by 1 day,
       is flagged ANTICIPATED (staying VALID), and the heir's output address is
       updated.
    3. Change a heir's AMOUNT, rebuild: the new will is anticipated by a further
       day, flagged ANTICIPATED, and the heir's output value is updated.
    4. Change a will-executor's base_fee, rebuild: the new will keeps the SAME
       locktime and updates the will-executor output, while the OLD transaction
       becomes UPDATED yet stays VALID.
    5. Every transaction state is assigned exactly per its description.

The network is switched to regtest at import time because the giovanna7
addresses are bech32 regtest addresses; this must happen before importing the
bal modules (``BalPlugin.chainname`` is computed at import).
"""

import itertools
import os
import sys
import time
import copy

import pytest
from unittest.mock import MagicMock, patch

from electrum import constants

sys.path.insert(0, os.path.dirname(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))


@pytest.fixture(autouse=True)
def _regtest_network():
    """Switch the global Electrum network to regtest for these tests.

    The giovanna7 addresses are bech32 *regtest* addresses, so the network must
    be regtest while building/validating transactions.  The previous network is
    restored afterwards so this module never pollutes mainnet-based tests that
    may run later in the same pytest process.
    """
    previous = constants.net
    constants.BitcoinRegtest.set_as_network()
    try:
        yield
    finally:
        previous.set_as_network()


import bal_fixtures as fx  # noqa: E402
from bal.core.heirs import (  # noqa: E402
    Heirs,
    HEIR_ADDRESS,
    HEIR_AMOUNT,
    HEIR_LOCKTIME,
    HEIR_REAL_AMOUNT,
)
from bal.core.will import Will, WillItem  # noqa: E402
from bal.core.util import Util  # noqa: E402
from bal.core.willexecutors import Willexecutors  # noqa: E402
from electrum.transaction import PartialTransaction  # noqa: E402

DAY = 86400


# ------------------------------------------------------------------ #
# Helpers
# ------------------------------------------------------------------ #

def _make_heirs(entries):
    """Build a ``Heirs`` instance (bypassing the wallet-backed ``__init__``).

    ``entries`` is ``{name: [address, amount, locktime]}``.
    """
    h = Heirs.__new__(Heirs)
    dict.__init__(h)
    h.db = MagicMock()
    h.wallet = MagicMock()
    h.update(copy.deepcopy(entries))
    return h


def _build(heirs, willexecutors, *, utxo_value=5_000_000, utxo_count=4,
           from_locktime=None, tx_fees=100, no_willexecutor=False):
    """Run the real builder and return the produced ``{txid: tx}`` mapping.

    ``PartialTransaction.txid`` is patched with a deterministic counter because
    the UTXOs are unsigned synthetic inputs whose real txid would be ``None``
    (this is the same technique used by the other mock tests).
    """
    wallet = fx.fake_wallet()
    utxos = fx.make_utxos(utxo_count, utxo_value)
    plugin = fx.fake_bal_plugin(willexecutors, no_willexecutor=no_willexecutor)
    if from_locktime is None:
        from_locktime = int(time.time())
    counter = itertools.count(1)

    def fake_txid(self):
        # Stable per-object txid: cache on the instance so repeated calls
        # within the builder return the same value, but distinct tx objects
        # get distinct ids.
        if not hasattr(self, "_test_txid"):
            self._test_txid = f"{next(counter):064x}"
        return self._test_txid

    with patch.object(Willexecutors, "get_willexecutors",
                      return_value=willexecutors), \
         patch.object(PartialTransaction, "txid", fake_txid):
        txs = heirs.get_transactions(plugin, wallet, tx_fees, utxos,
                                     from_locktime)
    return txs or {}


def _willexecutors(specs):
    """Build a ``{url: we_dict}`` mapping from ``[(url, address, base_fee)]``."""
    out = {}
    for url, address, base_fee in specs:
        out[url] = {
            "url": url,
            "address": address,
            "base_fee": base_fee,
            "selected": True,
            "status": 200,
        }
    return out


def _heir_output(tx, address):
    """Return the output value paid to ``address`` in ``tx`` (or None)."""
    for o in tx.outputs():
        if o.address == address:
            return o.value
    return None


def _backup_tx(txs):
    """Return the backup transaction (the one without a will-executor)."""
    for tx in txs.values():
        if not getattr(tx, "willexecutor", None):
            return tx
    return None


def _tx_for_we(txs, url):
    """Return the transaction built for the will-executor ``url``."""
    for tx in txs.values():
        we = getattr(tx, "willexecutor", None)
        if we and we.get("url") == url:
            return tx
    return None


# ------------------------------------------------------------------ #
# Test 1: amounts match heir outputs
# ------------------------------------------------------------------ #

def test_inheritance_amounts_match_outputs():
    """Each heir's resolved amount equals its output value in every built tx."""
    now = int(time.time())
    lt = now + 365 * DAY
    ha = fx.heir_addresses(3)
    wea = fx.willexecutor_addresses(2)

    heirs = _make_heirs({
        "alice": [ha[0], "20%", str(lt)],
        "bob":   [ha[1], "30%", str(lt)],
        "carol": [ha[2], "50%", str(lt)],
    })
    wes = _willexecutors([
        ("https://we1.example", wea[0], 100000),
        ("https://we2.example", wea[1], 50000),
    ])

    txs = _build(heirs, wes, no_willexecutor=True)
    assert txs, "no transactions built"

    # We expect one tx per selected will-executor plus one backup tx.
    assert _tx_for_we(txs, "https://we1.example") is not None
    assert _tx_for_we(txs, "https://we2.example") is not None
    assert _backup_tx(txs) is not None

    for txid, tx in txs.items():
        for name, hd in getattr(tx, "heirs", {}).items():
            real = hd[HEIR_REAL_AMOUNT]
            if isinstance(real, str) and "DUST" in real:
                continue
            out_val = _heir_output(tx, hd[HEIR_ADDRESS])
            assert out_val == real, (
                f"{name}: output {out_val} != resolved amount {real} "
                f"in tx {txid[:8]}"
            )
        # The will-executor fee output, if any, must equal its base_fee.
        we = getattr(tx, "willexecutor", None)
        if we:
            assert _heir_output(tx, we["address"]) == we["base_fee"], (
                f"will-executor {we['url']} output != base_fee"
            )


# ------------------------------------------------------------------ #
# Test 2 + 3 + 4: state transitions on update
# ------------------------------------------------------------------ #

def _single_we_will(heirs, url, address, base_fee, *, from_locktime, time_stamp,
                    txid=None):
    """Build a one-will-executor will and wrap it into a {txid: WillItem}.

    ``txid`` lets the caller pin a stable, unique transaction id (the synthetic
    unsigned tx has no real txid).  Returns ``(will_dict, txid, tx)`` for the
    single non-backup transaction.
    """
    wes = _willexecutors([(url, address, base_fee)])
    txs = _build(heirs, wes, from_locktime=from_locktime, no_willexecutor=False)
    tx = _tx_for_we(txs, url)
    assert tx is not None, "no will-executor transaction built"
    if txid is None:
        txid = tx._test_txid if hasattr(tx, "_test_txid") else tx.txid()
    item = WillItem.__new__(WillItem)
    item.tx = tx
    item._id = txid
    item.heirs = copy.deepcopy(tx.heirs)
    item.we = copy.deepcopy(tx.willexecutor)
    item.status = ""
    item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    item.tx_fees = 100
    item.time = time_stamp
    item.father = None
    item.children = {}
    item.description = ""
    item.change = ""
    item.set_status("VALID", True)
    return {txid: item}, txid, tx


def test_change_heir_address_anticipates_one_day():
    """Changing a heir's address rebuilds an anticipated (1 day) ANTICIPATED tx."""
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY  # midnight-aligned timestamp
    ha = fx.heir_addresses(4)

    # Original will.
    heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    old_will, old_txid, old_tx = _single_we_will(
        heirs, "https://we.example", ha[3], 100000,
        from_locktime=now, time_stamp=now - 10,
    )
    old_item = old_will[old_txid]
    old_locktime = old_item.tx.locktime
    assert _heir_output(old_tx, ha[0]) is not None

    # Rebuild with the heir's address changed.
    new_heirs = _make_heirs({"alice": [ha[1], "100%", str(lt)]})
    new_will, new_txid, new_tx = _single_we_will(
        new_heirs, "https://we.example", ha[3], 100000,
        from_locktime=now, time_stamp=now,
    )
    new_item = new_will[new_txid]

    # The new will item, compared against the old one sharing the same heirs
    # set but a different destination, should anticipate by 1 day.
    anticipated = new_item.set_anticipate(old_item)
    expected = int(Util.anticipate_locktime(old_locktime, days=1))

    assert anticipated, "new will should be anticipated"
    assert new_item.get_status("ANTICIPATED"), "ANTICIPATED must be set"
    assert new_item.get_status("VALID"), "VALID must remain set"
    assert int(new_item.tx.locktime) == expected, (
        f"locktime {new_item.tx.locktime} != anticipated {expected}"
    )
    # The new transaction pays the NEW address, not the old one.
    assert _heir_output(new_tx, ha[1]) is not None
    assert _heir_output(new_tx, ha[0]) is None


def test_change_heir_amount_anticipates_one_day():
    """Changing a heir's amount rebuilds an anticipated (1 day) ANTICIPATED tx."""
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)

    heirs = _make_heirs({
        "alice": [ha[0], "60%", str(lt)],
        "bob":   [ha[1], "40%", str(lt)],
    })
    old_will, old_txid, old_tx = _single_we_will(
        heirs, "https://we.example", ha[3], 100000,
        from_locktime=now, time_stamp=now - 10,
    )
    old_item = old_will[old_txid]
    old_locktime = old_item.tx.locktime
    old_alice_out = _heir_output(old_tx, ha[0])

    # Rebuild with alice's amount changed.
    new_heirs = _make_heirs({
        "alice": [ha[0], "80%", str(lt)],
        "bob":   [ha[1], "20%", str(lt)],
    })
    new_will, new_txid, new_tx = _single_we_will(
        new_heirs, "https://we.example", ha[3], 100000,
        from_locktime=now, time_stamp=now,
    )
    new_item = new_will[new_txid]

    anticipated = new_item.set_anticipate(old_item)
    expected = int(Util.anticipate_locktime(old_locktime, days=1))

    assert anticipated, "new will should be anticipated"
    assert new_item.get_status("ANTICIPATED"), "ANTICIPATED must be set"
    assert new_item.get_status("VALID"), "VALID must remain set"
    assert int(new_item.tx.locktime) == expected
    # alice's output value changed (80% > 60%).
    new_alice_out = _heir_output(new_tx, ha[0])
    assert new_alice_out is not None and new_alice_out != old_alice_out, (
        f"alice output should change: old {old_alice_out} new {new_alice_out}"
    )


def test_change_we_base_fee_keeps_locktime_and_marks_updated():
    """Changing only the will-executor base_fee keeps the locktime; old tx UPDATED.

    Per the spec: same locktime + same heirs but a new transaction -> the old
    transaction is flagged UPDATED while keeping VALID; the new transaction's
    will-executor output reflects the new base_fee.
    """
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})

    old_will, old_txid, old_tx = _single_we_will(
        heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now - 10, txid="d1" * 32,
    )
    old_item = old_will[old_txid]
    old_locktime = int(old_item.tx.locktime)

    # Rebuild with a HIGHER base_fee only.
    new_will, new_txid, new_tx = _single_we_will(
        heirs, "https://we.example", we_addr, 200000,
        from_locktime=now, time_stamp=now, txid="d2" * 32,
    )
    new_item = new_will[new_txid]

    # check_anticipate must keep the same locktime for a base_fee increase.
    kept = Will.check_anticipate(old_item, new_item)
    assert int(kept) == old_locktime, (
        f"locktime should stay {old_locktime}, got {kept}"
    )
    # set_anticipate returns False (no anticipation) since locktime is unchanged.
    assert not new_item.set_anticipate(old_item), (
        "no anticipation expected for a base_fee-only change"
    )
    assert int(new_item.tx.locktime) == old_locktime

    # The new will-executor output reflects the new base_fee.
    assert _heir_output(new_tx, we_addr) == 200000

    # Now place both transactions in one will (they share the same wallet UTXO)
    # and let the state engine flag the old one as UPDATED while keeping VALID.
    will = {old_txid: old_item, new_txid: new_item}
    all_inputs = Will.get_all_inputs(will, only_valid=True)
    Will.search_updated(all_inputs)

    assert old_item.get_status("UPDATED"), "old tx must be UPDATED"
    assert old_item.get_status("VALID"), "old tx must remain VALID"
    assert not new_item.get_status("UPDATED"), "new tx must NOT be UPDATED"
    assert new_item.get_status("VALID"), "new tx must remain VALID"


# ------------------------------------------------------------------ #
# Test 5: each state assigned per its description
# ------------------------------------------------------------------ #

def _wi(tx_locktime, heirs_data, txid, *, time_stamp=0):
    """Build a minimal VALID WillItem around a real PartialTransaction.

    The funding UTXO uses a txid *different* from this will item's own ``txid``
    so ``add_willtree`` does not mistake the item for its own parent.
    """
    utxo = fx.make_utxo("9e" * 32, 200_000)
    from electrum.transaction import PartialTxOutput
    addr = heirs_data[next(iter(heirs_data))][HEIR_ADDRESS]
    tx = PartialTransaction.from_io(
        [utxo],
        [PartialTxOutput.from_address_and_value(addr, 100_000)],
        locktime=tx_locktime, version=2,
    )
    item = WillItem.__new__(WillItem)
    item.tx = tx
    item._id = txid
    item.heirs = heirs_data
    item.we = None
    item.status = ""
    item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    item.tx_fees = 100
    item.time = time_stamp
    item.father = None
    item.children = {}
    item.description = ""
    item.change = ""
    item.set_status("VALID", True)
    return item


def test_state_anticipated_keeps_valid():
    now = int(time.time())
    lt = int((now + 30 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(2)
    old = _wi(lt, {"a": [ha[0], 100_000, str(lt)]}, "aa" * 32, time_stamp=now - 5)
    new = _wi(lt, {"a": [ha[1], 100_000, str(lt)]}, "bb" * 32, time_stamp=now)
    assert new.set_anticipate(old)
    assert new.get_status("ANTICIPATED")
    assert new.get_status("VALID")  # VALID is NOT cleared


def test_state_replaced_clears_valid_and_propagates():
    """REPLACED: input spent by a lower-locktime tx; cascades to children."""
    now = int(time.time())
    lt_parent = now + 60 * DAY
    lt_child = now + 30 * DAY
    lt_replacer = now + 15 * DAY
    ha = fx.heir_addresses(3)
    from electrum.transaction import PartialTxOutput, PartialTxInput, TxOutpoint
    from electrum.util import bfh

    parent_txid = "aa" * 32
    parent_tx = PartialTransaction.from_io(
        [fx.make_utxo("aa" * 32, 200_000)],
        [PartialTxOutput.from_address_and_value(ha[0], 100_000),
         PartialTxOutput.from_address_and_value(fx.change_address(), 99_000)],
        locktime=lt_parent, version=2,
    )
    pwi = WillItem.__new__(WillItem)
    pwi.tx = parent_tx; pwi._id = parent_txid
    pwi.heirs = {"a": [ha[0], 100_000, str(lt_parent)]}; pwi.we = None
    pwi.status = ""; pwi.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    pwi.tx_fees = 100; pwi.time = now; pwi.father = None; pwi.children = {}
    pwi.description = ""; pwi.change = ""
    pwi.set_status("VALID", True)

    ci = PartialTxInput(prevout=TxOutpoint(txid=bfh(parent_txid), out_idx=1))
    ci._trusted_value_sats = 99_000; ci.is_mine = True
    child_tx = PartialTransaction.from_io(
        [ci], [PartialTxOutput.from_address_and_value(ha[1], 50_000)],
        locktime=lt_child, version=2,
    )
    cwi = WillItem.__new__(WillItem)
    cwi.tx = child_tx; cwi._id = "bb" * 32
    cwi.heirs = {"b": [ha[1], 50_000, str(lt_child)]}; cwi.we = None
    cwi.status = ""; cwi.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    cwi.tx_fees = 100; cwi.time = now; cwi.father = None; cwi.children = {}
    cwi.description = ""; cwi.change = ""
    cwi.set_status("VALID", True)

    replacer_tx = PartialTransaction.from_io(
        [fx.make_utxo("aa" * 32, 200_000)],
        [PartialTxOutput.from_address_and_value(ha[2], 150_000)],
        locktime=lt_replacer, version=2,
    )
    rwi = WillItem.__new__(WillItem)
    rwi.tx = replacer_tx; rwi._id = "cc" * 32
    rwi.heirs = {"c": [ha[2], 150_000, str(lt_replacer)]}; rwi.we = None
    rwi.status = ""; rwi.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    rwi.tx_fees = 100; rwi.time = now; rwi.father = None; rwi.children = {}
    rwi.description = ""; rwi.change = ""
    rwi.set_status("VALID", True)

    will = {pwi._id: pwi, cwi._id: cwi, rwi._id: rwi}
    Will.add_willtree(will)
    Will.search_rai(Will.get_all_inputs(will, only_valid=True),
                    [fx.make_utxo("aa" * 32, 200_000)], will, fx.fake_wallet())

    assert pwi.get_status("REPLACED") and not pwi.get_status("VALID")
    assert cwi.get_status("REPLACED") and not cwi.get_status("VALID")
    assert rwi.get_status("VALID") and not rwi.get_status("REPLACED")


def test_state_invalidated_clears_valid():
    """INVALIDATED: input spent elsewhere and parent tx not in the will."""
    now = int(time.time())
    lt = now + 30 * DAY
    ha = fx.heir_addresses(1)
    from electrum.transaction import PartialTxOutput, PartialTxInput, TxOutpoint
    from electrum.util import bfh

    txin = PartialTxInput(prevout=TxOutpoint(txid=bfh("dd" * 32), out_idx=0))
    txin._trusted_value_sats = 100_000; txin.is_mine = True
    tx = PartialTransaction.from_io(
        [txin], [PartialTxOutput.from_address_and_value(ha[0], 90_000)],
        locktime=lt, version=2,
    )
    wi = WillItem.__new__(WillItem)
    wi.tx = tx; wi._id = "ee" * 32
    wi.heirs = {"a": [ha[0], 90_000, str(lt)]}; wi.we = None
    wi.status = ""; wi.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    wi.tx_fees = 100; wi.time = now; wi.father = None; wi.children = {}
    wi.description = ""; wi.change = ""
    wi.set_status("VALID", True)

    will = {wi._id: wi}
    Will.add_willtree(will)
    # all_utxos empty -> input is gone; wallet has no record of our tx.
    Will.search_rai(Will.get_all_inputs(will, only_valid=True), [], will,
                    fx.fake_wallet())

    assert wi.get_status("INVALIDATED")
    assert not wi.get_status("VALID")


def test_state_pending_clears_valid():
    """PENDING: our tx is seen unconfirmed in the wallet (height 0)."""
    now = int(time.time())
    lt = now + 30 * DAY
    ha = fx.heir_addresses(1)
    wi = _wi(lt, {"a": [ha[0], 100_000, str(lt)]}, "ab" * 32, time_stamp=now)

    wallet = fx.fake_wallet()
    wallet.get_tx_info.return_value.tx_mined_status.height.return_value = 0
    will = {wi._id: wi}
    Will.add_willtree(will)
    # utxos_list empty -> the input is spent (our tx is in mempool spending it).
    Will.check_invalidated(will, [], wallet)

    assert wi.get_status("PENDING")
    assert not wi.get_status("VALID")


def test_state_confirmed_clears_valid():
    """CONFIRMED: our tx is mined (height > 0)."""
    now = int(time.time())
    lt = now + 30 * DAY
    ha = fx.heir_addresses(1)
    wi = _wi(lt, {"a": [ha[0], 100_000, str(lt)]}, "ac" * 32, time_stamp=now)

    wallet = fx.fake_wallet()
    wallet.get_tx_info.return_value.tx_mined_status.height.return_value = 123
    will = {wi._id: wi}
    Will.add_willtree(will)
    Will.check_invalidated(will, [], wallet)

    assert wi.get_status("CONFIRMED")
    assert not wi.get_status("VALID")


def test_state_updated_keeps_valid():
    """UPDATED: same locktime + same heirs, older tx flagged UPDATED, stays VALID."""
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(1)
    from electrum.transaction import PartialTxOutput

    shared_utxo = lambda: fx.make_utxo("fa" * 32, 500_000)
    older = WillItem.__new__(WillItem)
    older.tx = PartialTransaction.from_io(
        [shared_utxo()],
        [PartialTxOutput.from_address_and_value(ha[0], 100_000),
         PartialTxOutput.from_address_and_value(fx.willexecutor_addresses(1)[0], 100_000)],
        locktime=lt, version=2)
    older._id = "f1" * 32
    older.heirs = {"a": [ha[0], 100_000, str(lt)]}
    older.we = {"url": "https://we.example", "base_fee": 100000}
    older.status = ""; older.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    older.tx_fees = 100; older.time = now - 100; older.father = None
    older.children = {}; older.description = ""; older.change = ""
    older.set_status("VALID", True)

    newer = WillItem.__new__(WillItem)
    newer.tx = PartialTransaction.from_io(
        [shared_utxo()],
        [PartialTxOutput.from_address_and_value(ha[0], 100_000),
         PartialTxOutput.from_address_and_value(fx.willexecutor_addresses(1)[0], 200_000)],
        locktime=lt, version=2)
    newer._id = "f2" * 32
    newer.heirs = {"a": [ha[0], 100_000, str(lt)]}
    newer.we = {"url": "https://we.example", "base_fee": 200000}
    newer.status = ""; newer.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
    newer.tx_fees = 100; newer.time = now; newer.father = None
    newer.children = {}; newer.description = ""; newer.change = ""
    newer.set_status("VALID", True)

    will = {older._id: older, newer._id: newer}
    Will.search_updated(Will.get_all_inputs(will, only_valid=True))

    assert older.get_status("UPDATED") and older.get_status("VALID")
    assert not newer.get_status("UPDATED") and newer.get_status("VALID")


# ------------------------------------------------------------------ #

if __name__ == "__main__":
    import inspect
    for name, obj in sorted(globals().items()):
        if name.startswith("test_") and inspect.isfunction(obj):
            obj()
            print(f"  [OK] {name}")
    print("[OK] All inheritance-flow tests passed")
