"""
Test: no invalidation is requested when time passes with unchanged settings.

Scenario:
    The user creates a will with RELATIVE locktime ("1y") and RELATIVE threshold
    ("30d"), signs it and pushes it to the will-executor.  After 2 days, they
    run Check again.  The system must NOT raise any of:

        * CheckAliveError  (threshold resolved against ``now`` → always future)
        * WillExpiredException  (locktime resolved against *creation time* →
          stable, does not drift)
        * WillPostponedException  (same locktime → no postpone detected)

    This verifies that the "cancel/invalidate" prompt never appears
    spuriously when the user simply lets time pass without changing any setting.
"""

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, WillExpiredException, WillPostponedException,
)
from bal.core.util import Util  # noqa: E402
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 (same patterns as test_inheritance_flow.py)
# ------------------------------------------------------------------ #

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,
           no_willexecutor=False):
    wallet = fx.fake_wallet()
    utxos = fx.make_utxos(4, 5_000_000)
    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):
        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 actual test
# ------------------------------------------------------------------ #

def test_no_invalidation_after_two_days_with_relative_settings():
    """No invalidation after 2 days when locktime/threshold are RELATIVE strings.

    This is the critical scenario the user reported: a will created with
    "1y" locktime and "30d" threshold (both relative), signed and pushed,
    should NOT trigger any cancellation request when Check is re-run 2 days
    later with no settings changed.
    """
    now = int(time.time())
    ha = fx.heir_addresses(3)
    we_addr = fx.willexecutor_addresses(1)[0]

    # ── 1. Create heirs with RELATIVE locktime "1y" ──────────────────
    heirs_dict = {
        "alice": [ha[0], "40%", "1y"],
        "bob":   [ha[1], "30%", "1y"],
        "carol": [ha[2], "30%", "1y"],
    }
    h = Heirs.__new__(Heirs)
    dict.__init__(h)
    h.db = MagicMock()
    h.wallet = MagicMock()
    h.update(copy.deepcopy(heirs_dict))

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

    # ── 2. Build the will ───────────────────────────────────────────
    txs = _build(h, 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 a WillItem, mark as SIGNED (COMPLETE + PUSHED) ──
    # IMPORTANT: use copy.deepcopy(tx.heirs) — the builder stores the
    # original percentage strings (not resolved integers), and the
    # postpone comparison in check_willexecutors_and_heirs expects
    # those same strings so they can be compared with the current
    # heirs dict entries.
    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          # will creation time — the stable base for "1y"
    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)

    # ── 4. Baseline: check flow at ORIGINAL time (must pass) ────────
    check_date = BalTimestamp("30d").to_timestamp()
    assert check_date > now, "baseline: threshold is in the future"

    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)

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

    # BalTimestamp("30d").to_timestamp() calls datetime.now() internally.
    # Patch only that module's reference so fromtimestamp etc. still work.
    import bal.core.plugin_base as pb_mod
    import datetime as real_dt

    # We need the patched datetime to proxy fromtimestamp/timedelta to the
    # real ones, otherwise BalTimestamp._safe_fromtimestamp and timedelta
    # arithmetic break.
    class _PatchedDt:
        """Minimal proxy: only .now() returns the advanced time, everything
        else delegates to the real datetime class."""
        @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()

    # The threshold resolved against the advanced "now" is STILL future.
    assert check_date_2d > advanced_now_dt.timestamp(), (
        "30d threshold is still in the future even after 2 days"
    )

    # ── 6. check_will_expired — no drift ────────────────────────────
    # The existing tx locktime was resolved from "1y" against from_locktime
    # (= the original `now`) during build, so it is immutable.  The check
    # compares it against check_date_2d (which is *future*), so it never
    # raises WillExpiredException.
    all_inputs_2d = Will.get_all_inputs(willitems, only_valid=True)
    all_inputs_min_2d = Will.get_all_inputs_min_locktime(all_inputs_2d)
    Will.check_will_expired(all_inputs_min_2d, check_date_2d)

    # ── 7. check_willexecutors_and_heirs — no spurious postpone ─────
    # The function resolves "1y" against w.time (= the original `now`),
    # giving the same absolute locktime as tx.locktime → no postpone.
    Will.check_willexecutors_and_heirs(
        willitems, h, wes,
        False,    # no_willexecutor
        check_date_2d,
        100,
    )

    # If we reach here, no invalidation was requested. ✓
