"""
Test: modifying an heir (address or amount) on a signed/pushed will triggers
a rebuild (HeirNotFoundException) but NOT an on-chain invalidation transaction.

Scenario:
    The user has a signed/pushed will.  Changing the address or the
    percentage/amount of an existing heir changes the transaction's output
    script or value, so the old transaction must be rebuilt.

    In ``check_willexecutors_and_heirs``, the condition
    ``heir[0] == their[0] and heir[1] == their[1]`` (address and amount) is
    False for the modified heir → that heir is never counted in
    ``heirs_found`` → ``HeirNotFoundException`` is raised at line 911.

    This holds as long as the locktime is not also postponed (which would
    raise ``WillPostponedException`` instead, requesting an on-chain
    invalidation transaction).

    The threshold ("30d") also stays in the future, so ``CheckAliveError`` /
    ``WillExpiredException`` must not fire either.

    Two sub-tests:

    1. ``test_change_address_triggers_rebuild``
       - Same percentage, same locktime → only the address differs.

    2. ``test_change_amount_triggers_rebuild``
       - Same address, same locktime → only the percentage/amount differs.

    Both must raise ``HeirNotFoundException`` and NOT
    ``WillPostponedException``.
"""

import copy
import os
import sys
import time
import itertools

import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime

from electrum import constants

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


@pytest.fixture(autouse=True)
def _regtest_network():
    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 Heirs  # noqa: E402
from bal.core.will import (  # noqa: E402
    Will, WillItem, HeirNotFoundException,
    WillPostponedException, WillExpiredException,
)
from bal.core.willexecutors import Willexecutors  # noqa: E402
from bal.core.plugin_base import BalTimestamp  # noqa: E402
from electrum.transaction import PartialTransaction  # noqa: E402

DAY = 86400


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

def _willexecutors(specs):
    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 _build(heirs, willexecutors, *, from_locktime=None, tx_fees=100):
    wallet = fx.fake_wallet()
    utxos = fx.make_utxos(4, 5_000_000)
    plugin = fx.fake_bal_plugin(willexecutors)
    if from_locktime is None:
        from_locktime = int(time.time())
    counter = itertools.count(1)

    def fake_txid(self):
        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 _tx_for_we(txs, url):
    for tx in txs.values():
        we = getattr(tx, "willexecutor", None)
        if we and we.get("url") == url:
            return tx
    return None


def _build_signed_pushed_will(heirs_dict, wes, now):
    """Build, sign (COMPLETE) and push (PUSHED) a will.  Return the willitems
    dict and the will-item creation time *w.time*."""
    txs = _build(heirs_dict, wes, from_locktime=now)
    tx = _tx_for_we(txs, "https://we.example")
    assert tx is not None, "no will-executor transaction built"
    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 = now
    item.father = None
    item.children = {}
    item.description = ""
    item.change = ""
    item.set_status("VALID", True)
    item.set_status("COMPLETE", True)
    item.set_status("PUSHED", True)

    return {txid: item}, now


# ------------------------------------------------------------------ #
# Tests
# ------------------------------------------------------------------ #

def test_change_address_triggers_rebuild():
    """Changing the address of an existing heir triggers HeirNotFoundException
    (plain rebuild), NOT WillPostponedException (on-chain invalidation)."""
    now = int(time.time())
    ha = fx.heir_addresses(4)
    we_addr = fx.willexecutor_addresses(1)[0]
    wes = _willexecutors([("https://we.example", we_addr, 100000)])

    addr_alice_old = ha[0]
    addr_alice_new = ha[3]  # different address

    # ── 1. Build signed/pushed will ─────────────────────────────────
    heirs_initial = {
        "alice": [addr_alice_old, "40%", "300d"],
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_initial = Heirs.__new__(Heirs)
    dict.__init__(h_initial)
    h_initial.db = MagicMock()
    h_initial.wallet = MagicMock()
    h_initial.update(copy.deepcopy(heirs_initial))

    willitems, w_time = _build_signed_pushed_will(h_initial, wes, now)

    # ── 2. Change only alice's address ──────────────────────────────
    heirs_changed = {
        "alice": [addr_alice_new, "40%", "300d"],  # same %, same locktime
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_changed = Heirs.__new__(Heirs)
    dict.__init__(h_changed)
    h_changed.db = MagicMock()
    h_changed.wallet = MagicMock()
    h_changed.update(copy.deepcopy(heirs_changed))

    check_date = BalTimestamp("30d").to_timestamp()

    # ── 3. Assert HeirNotFoundException, NOT WillPostponedException ─
    with pytest.raises(HeirNotFoundException) as exc_info:
        Will.check_willexecutors_and_heirs(
            willitems, h_changed, wes,
            False,      # no_willexecutor
            check_date,
            100,
        )

    # The exception should name the modified heir.
    assert "alice" in str(exc_info.value)

    # Double-check: WillPostponedException must NOT be raised.
    try:
        Will.check_willexecutors_and_heirs(
            willitems, h_changed, wes,
            False,
            check_date,
            100,
        )
    except HeirNotFoundException:
        pass  # expected
    except WillPostponedException:
        pytest.fail("changing address must NOT request an invalidation "
                     "transaction (WillPostponedException)")
    except Exception:
        pass  # other rebuild-related exceptions are acceptable


def test_change_amount_triggers_rebuild():
    """Changing the percentage/amount of an existing heir triggers
    HeirNotFoundException (plain rebuild), NOT WillPostponedException (on-chain
    invalidation)."""
    now = int(time.time())
    ha = fx.heir_addresses(3)
    we_addr = fx.willexecutor_addresses(1)[0]
    wes = _willexecutors([("https://we.example", we_addr, 100000)])

    # ── 1. Build signed/pushed will ─────────────────────────────────
    heirs_initial = {
        "alice": [ha[0], "40%", "300d"],
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_initial = Heirs.__new__(Heirs)
    dict.__init__(h_initial)
    h_initial.db = MagicMock()
    h_initial.wallet = MagicMock()
    h_initial.update(copy.deepcopy(heirs_initial))

    willitems, w_time = _build_signed_pushed_will(h_initial, wes, now)

    # ── 2. Change only alice's percentage ───────────────────────────
    heirs_changed = {
        "alice": [ha[0], "50%", "300d"],  # same addr, same locktime
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_changed = Heirs.__new__(Heirs)
    dict.__init__(h_changed)
    h_changed.db = MagicMock()
    h_changed.wallet = MagicMock()
    h_changed.update(copy.deepcopy(heirs_changed))

    check_date = BalTimestamp("30d").to_timestamp()

    # ── 3. Assert HeirNotFoundException, NOT WillPostponedException ─
    with pytest.raises(HeirNotFoundException) as exc_info:
        Will.check_willexecutors_and_heirs(
            willitems, h_changed, wes,
            False,
            check_date,
            100,
        )

    assert "alice" in str(exc_info.value)

    try:
        Will.check_willexecutors_and_heirs(
            willitems, h_changed, wes,
            False,
            check_date,
            100,
        )
    except HeirNotFoundException:
        pass  # expected
    except WillPostponedException:
        pytest.fail("changing amount must NOT request an invalidation "
                     "transaction (WillPostponedException)")
    except Exception:
        pass
