"""
Test: anticipating a signed will does NOT request an invalidation transaction.

Scenario:
    The user has a signed/pushed will with RELATIVE locktime "300d".  After 2
    days they change the locktime to "100d" (shorter → anticipate).

    Anticipating means the new inheritance would unlock EARLIER than the
    already-signed one.  Unlike a postpone, this does NOT need the old
    pre-signed transaction to be invalidated on-chain first: a will-executor
    holding the old "300d" tx can only broadcast it LATER than the new "100d"
    one, so there is no risk of executing the inheritance too early.

    Therefore ``check_willexecutors_and_heirs`` must NOT raise
    ``WillPostponedException`` (the only exception that requests an on-chain
    invalidation transaction).  It may request a plain rebuild
    (``HeirNotFoundException``) but never an invalidation.

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

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, 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


# ------------------------------------------------------------------ #
# The test
# ------------------------------------------------------------------ #

def test_anticipate_after_two_days_does_not_request_invalidation():
    """Anticipating (300d -> 100d) after 2 days must NOT raise
    WillPostponedException (no on-chain invalidation transaction)."""
    now = int(time.time())
    ha = fx.heir_addresses(3)
    we_addr = fx.willexecutor_addresses(1)[0]

    # ── 1. Create heirs with INITIAL locktime "300d" ─────────────────
    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))

    wes = _willexecutors([("https://we.example", we_addr, 100000)])

    # ── 2. Build the will ───────────────────────────────────────────
    txs = _build(h_initial, 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()

    # ── 3. Wrap in WillItem, mark SIGNED + PUSHED ───────────────────
    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)   # ← signed
    item.set_status("PUSHED", True)     # ← sent to will-executor

    willitems = {txid: item}
    old_locktime = int(tx.locktime)

    # The frozen tx locktime is ~ now + 300d.
    assert old_locktime > now + 290 * DAY

    # ── 4. Advance "now" by 2 days ──────────────────────────────────
    advanced_now_dt = datetime.fromtimestamp(now + 2 * DAY)

    import bal.core.plugin_base as pb_mod
    import datetime as real_dt

    class _PatchedDt:
        @classmethod
        def now(cls):
            return advanced_now_dt
        @classmethod
        def fromtimestamp(cls, ts):
            return real_dt.datetime.fromtimestamp(ts)
        @classmethod
        def today(cls):
            return advanced_now_dt.date()
    _PatchedDt.timedelta = real_dt.timedelta

    with patch.object(pb_mod, "datetime", _PatchedDt):
        check_date_2d = BalTimestamp("30d").to_timestamp()

    # ── 5. Threshold still in future → no CheckAliveError ───────────
    assert check_date_2d > advanced_now_dt.timestamp(), (
        "30d threshold is still future after 2 days"
    )

    # ── 6. check_will_expired — old locktime (now+300d) > check_date ─
    # The frozen 300d locktime is well beyond the 30d threshold, so the
    # already-signed will is NOT expired and no anticipation is forced here.
    all_inputs = Will.get_all_inputs(willitems, only_valid=True)
    all_inputs_min = Will.get_all_inputs_min_locktime(all_inputs)
    Will.check_will_expired(all_inputs_min, check_date_2d)

    # ── 7. check_willexecutors_and_heirs with SHORTER locktime ──────
    # Change the locktime from "300d" to "100d" (anticipate).
    heirs_changed = {
        "alice": [ha[0], "40%", "100d"],
        "bob":   [ha[1], "30%", "100d"],
        "carol": [ha[2], "30%", "100d"],
    }
    h_changed = Heirs.__new__(Heirs)
    dict.__init__(h_changed)
    h_changed.db = MagicMock()
    h_changed.wallet = MagicMock()
    h_changed.update(copy.deepcopy(heirs_changed))

    # The new locktime (now+100d) is EARLIER than the frozen one (now+300d):
    new_locktime = (
        real_dt.datetime.fromtimestamp(now) + real_dt.timedelta(days=100)
    ).timestamp()
    assert new_locktime < old_locktime, "100d must anticipate 300d"

    # The whole point: NO WillPostponedException is raised (anticipation does
    # not need an on-chain invalidation transaction).
    try:
        Will.check_willexecutors_and_heirs(
            willitems, h_changed, wes,
            False,      # no_willexecutor
            check_date_2d,
            100,         # tx_fees (unchanged)
        )
    except WillPostponedException as exc:
        pytest.fail(
            f"anticipation must NOT request an invalidation transaction, "
            f"but WillPostponedException was raised: {exc}"
        )
    except WillExpiredException as exc:
        pytest.fail(
            f"anticipation within the threshold must NOT expire the will, "
            f"but WillExpiredException was raised: {exc}"
        )
    except Exception:
        # A plain rebuild path (e.g. HeirNotFoundException) is acceptable:
        # it does NOT request an on-chain invalidation transaction.
        pass
