"""
Regression tests for the high-level inheritance rules described in the
project documentation (``docs/inheritance-options.md``).

These tests verify the decision flow for changing a will:

    * an unsigned old will is simply replaced, never invalidated on-chain;
    * adding / changing / removing an heir triggers anticipation (1 day earlier);
    * adding or changing a will-executor rebuilds the tx while keeping the same
      locktime;
    * postponing an already signed will requires on-chain invalidation;
    * a Check-Alive threshold in the past triggers invalidation;
    * relative locktimes are compared against the will creation time, not
      against the current wall-clock time.

The tests use the same mock fixtures as the rest of the suite: genuine regtest
addresses from the giovanna7 wallet, synthetic UTXOs and a patched
``PartialTransaction.txid`` so the builder can run without a live Electrum.
"""

import copy
import datetime as real_dt
import itertools
import os
import sys
import time

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

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():
    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, HeirNotFoundException,
    NotCompleteWillException,
)
from bal.core.willexecutors import Willexecutors  # noqa: E402
from bal.core.util import Util  # noqa: E402
from bal.core.plugin_base import BalTimestamp  # noqa: E402
from electrum.transaction import PartialTransaction  # noqa: E402

DAY = 86400
_TXID_COUNTER = itertools.count(1)


# ------------------------------------------------------------------ #
# 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 _make_heirs(entries):
    h = Heirs.__new__(Heirs)
    dict.__init__(h)
    h.db = MagicMock()
    h.wallet = MagicMock()
    h.update(copy.deepcopy(entries))
    return h


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


def _backup_tx(txs):
    for tx in txs.values():
        if not getattr(tx, "willexecutor", None):
            return tx
    return None


def _item_from_tx(tx, *, txid=None, complete=False, pushed=False, time=None,
                  we=None, heirs=None):
    if txid is None:
        txid = getattr(tx, "_test_txid", None) or f"tx{next(_TXID_COUNTER):08x}"
    if heirs is None:
        heirs = copy.deepcopy(getattr(tx, "heirs", {}))
    if we is None and hasattr(tx, "willexecutor"):
        we = copy.deepcopy(tx.willexecutor)
    d = {
        "tx": tx,
        "heirs": heirs,
        "willexecutor": we,
        "status": "",
        "description": getattr(tx, "description", ""),
        "time": time,
        "change": "",
        "baltx_fees": getattr(tx, "tx_fees", 100),
    }
    item = WillItem(d, _id=txid)
    item.set_status("VALID", True)
    if complete:
        item.set_status("COMPLETE", True)
    if pushed:
        item.set_status("PUSHED", True)
    return item


def _build_item(heirs, url, address, base_fee, *, from_locktime, time_stamp,
                complete=False, pushed=False):
    wes = _willexecutors([(url, address, base_fee)])
    txs = _build(heirs, wes, from_locktime=from_locktime)
    tx = _tx_for_we(txs, url)
    assert tx is not None, "no will-executor transaction built"
    txid = getattr(tx, "_test_txid", None) or f"tx{next(_TXID_COUNTER):08x}"
    return _item_from_tx(tx, txid=txid, complete=complete, pushed=pushed,
                         time=time_stamp), txid, tx


# ------------------------------------------------------------------ #
# 1. Unsigned old will is replaced, not invalidated
# ------------------------------------------------------------------ #

def test_unsigned_will_postpone_triggers_rebuild_not_invalidation():
    """Postponing a will that was never signed/sent does NOT raise
    WillPostponedException: it is treated as a plain rebuild."""
    now = int(time.time())
    ha = fx.heir_addresses(3)
    we_addr = fx.willexecutor_addresses(1)[0]
    wes = _willexecutors([("https://we.example", we_addr, 100000)])

    heirs_initial = {
        "alice": [ha[0], "40%", "300d"],
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_initial = _make_heirs(heirs_initial)

    txs = _build(h_initial, wes, from_locktime=now)
    tx = _tx_for_we(txs, "https://we.example")
    txid = getattr(tx, "_test_txid", None) or f"tx{next(_TXID_COUNTER):08x}"
    item = _item_from_tx(tx, txid=txid, complete=False, pushed=False, time=now)

    heirs_postponed = _make_heirs({
        "alice": [ha[0], "40%", "1y"],
        "bob":   [ha[1], "30%", "1y"],
        "carol": [ha[2], "30%", "1y"],
    })
    check_date = BalTimestamp("30d").to_timestamp()

    with pytest.raises(NotCompleteWillException) as exc_info:
        Will.check_willexecutors_and_heirs(
            {txid: item}, heirs_postponed, wes, False, check_date, 100
        )
    assert not isinstance(exc_info.value, WillPostponedException)


def test_unsigned_will_change_is_anticipated():
    """A change to an unsigned will is anticipated (replaced), not invalidated."""
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    old_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    old_item, _, old_tx = _build_item(
        old_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now - 10,
        complete=False, pushed=False,
    )

    new_heirs = _make_heirs({"alice": [ha[1], "100%", str(lt)]})
    new_item, _, _ = _build_item(
        new_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now,
        complete=False, pushed=False,
    )

    assert new_item.set_anticipate(old_item)
    assert new_item.get_status("ANTICIPATED")
    assert int(new_item.tx.locktime) == int(
        Util.anticipate_locktime(old_tx.locktime, days=1)
    )


# ------------------------------------------------------------------ #
# 2. Adding / changing / removing an heir anticipates the locktime
# ------------------------------------------------------------------ #

def test_add_heir_anticipates_locktime():
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    old_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    old_item, _, old_tx = _build_item(
        old_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now - 10,
        complete=True, pushed=True,
    )

    new_heirs = _make_heirs({
        "alice": [ha[0], "60%", str(lt)],
        "bob":   [ha[1], "40%", str(lt)],
    })
    new_item, _, _ = _build_item(
        new_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now,
        complete=False, pushed=False,
    )

    assert new_item.set_anticipate(old_item)
    assert new_item.get_status("ANTICIPATED")
    assert int(new_item.tx.locktime) == int(
        Util.anticipate_locktime(old_tx.locktime, days=1)
    )


def test_change_heir_amount_anticipates_locktime():
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    old_heirs = _make_heirs({
        "alice": [ha[0], "60%", str(lt)],
        "bob":   [ha[1], "40%", str(lt)],
    })
    old_item, _, old_tx = _build_item(
        old_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now - 10,
        complete=True, pushed=True,
    )

    new_heirs = _make_heirs({
        "alice": [ha[0], "80%", str(lt)],
        "bob":   [ha[1], "20%", str(lt)],
    })
    new_item, _, _ = _build_item(
        new_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now,
        complete=False, pushed=False,
    )

    assert new_item.set_anticipate(old_item)
    assert new_item.get_status("ANTICIPATED")
    assert int(new_item.tx.locktime) == int(
        Util.anticipate_locktime(old_tx.locktime, days=1)
    )


def test_remove_heir_anticipates_locktime():
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    old_heirs = _make_heirs({
        "alice": [ha[0], "60%", str(lt)],
        "bob":   [ha[1], "40%", str(lt)],
    })
    old_item, _, old_tx = _build_item(
        old_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now - 10,
        complete=True, pushed=True,
    )

    new_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    new_item, _, _ = _build_item(
        new_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now,
        complete=False, pushed=False,
    )

    assert new_item.set_anticipate(old_item)
    assert new_item.get_status("ANTICIPATED")
    assert int(new_item.tx.locktime) == int(
        Util.anticipate_locktime(old_tx.locktime, days=1)
    )


# ------------------------------------------------------------------ #
# 3. Adding or changing a will-executor keeps the locktime
# ------------------------------------------------------------------ #

def test_add_willexecutor_keeps_locktime():
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    # Old will: local backup only, no will-executor.
    old_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    old_txs = _build(old_heirs, {}, from_locktime=now, no_willexecutor=True)
    old_tx = _backup_tx(old_txs)
    assert old_tx is not None, "no backup transaction built"
    old_item = _item_from_tx(
        old_tx, complete=True, pushed=True, time=now - 10, we=None
    )
    old_locktime = int(old_tx.locktime)

    # New will: same heir, same locktime, but now with a will-executor.
    new_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    new_item, _, _ = _build_item(
        new_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now,
        complete=False, pushed=False,
    )

    kept = Will.check_anticipate(old_item, new_item)
    assert int(kept) == old_locktime
    assert not new_item.set_anticipate(old_item)
    assert int(new_item.tx.locktime) == old_locktime


def test_modify_willexecutor_keeps_locktime():
    now = int(time.time())
    lt = int((now + 365 * DAY) / DAY) * DAY
    ha = fx.heir_addresses(4)
    we_addr = ha[3]

    old_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    old_item, _, old_tx = _build_item(
        old_heirs, "https://we.example", we_addr, 100000,
        from_locktime=now, time_stamp=now - 10,
        complete=True, pushed=True,
    )
    old_locktime = int(old_tx.locktime)

    # Same URL, higher base_fee -> locktime must stay the same.
    new_heirs = _make_heirs({"alice": [ha[0], "100%", str(lt)]})
    new_item, _, _ = _build_item(
        new_heirs, "https://we.example", we_addr, 150000,
        from_locktime=now, time_stamp=now,
        complete=False, pushed=False,
    )

    kept = Will.check_anticipate(old_item, new_item)
    assert int(kept) == old_locktime
    assert not new_item.set_anticipate(old_item)
    assert int(new_item.tx.locktime) == old_locktime


# ------------------------------------------------------------------ #
# 4. Postponing a signed will requires invalidation
# ------------------------------------------------------------------ #

def test_postpone_signed_will_requires_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)])

    heirs_initial = {
        "alice": [ha[0], "40%", "300d"],
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_initial = _make_heirs(heirs_initial)

    txs = _build(h_initial, wes, from_locktime=now)
    tx = _tx_for_we(txs, "https://we.example")
    txid = getattr(tx, "_test_txid", None) or f"tx{next(_TXID_COUNTER):08x}"
    willitems = _item_from_tx(tx, txid=txid, complete=True, pushed=True,
                              time=now)

    heirs_postponed = _make_heirs({
        "alice": [ha[0], "40%", "1y"],
        "bob":   [ha[1], "30%", "1y"],
        "carol": [ha[2], "30%", "1y"],
    })
    check_date = BalTimestamp("30d").to_timestamp()

    with pytest.raises(WillPostponedException):
        Will.check_willexecutors_and_heirs(
            {txid: willitems}, heirs_postponed, wes, False, check_date, 100
        )


# ------------------------------------------------------------------ #
# 5. Check-Alive threshold in the past triggers invalidation
# ------------------------------------------------------------------ #

def test_threshold_in_past_raises_check_alive_error():
    """A Check-Alive threshold that is already in the past makes
    ``BalWindow.init_class_variables`` raise ``CheckAliveError``."""
    from bal.gui.qt.window import BalWindow
    from bal.gui.qt.common import CheckAliveError
    import bal.gui.qt.window as window_mod

    now = int(time.time())
    bw = object.__new__(BalWindow)
    bw.heirs = {"alice": [fx.heir_addresses(1)[0], "100%", "300d"]}
    bw.will_settings = {"threshold": now - 1000, "locktime": "1y"}
    bw.bal_plugin = MagicMock()
    bw.bal_plugin.NO_WILLEXECUTOR.get.return_value = False
    bw.bal_plugin.ENABLE_MULTIVERSE.get.return_value = False
    bw.willexecutors = {}

    class _PatchedDt:
        @classmethod
        def now(cls):
            return real_dt.datetime.fromtimestamp(now)

        @classmethod
        def fromtimestamp(cls, ts):
            return real_dt.datetime.fromtimestamp(ts)

        @classmethod
        def today(cls):
            return real_dt.datetime.fromtimestamp(now).date()

    _PatchedDt.timedelta = real_dt.timedelta

    with patch.object(window_mod, "datetime", _PatchedDt), \
         patch.object(window_mod, "Willexecutors") as mock_we:
        mock_we.get_willexecutors.return_value = {}
        with pytest.raises(CheckAliveError):
            bw.init_class_variables()


# ------------------------------------------------------------------ #
# 6. Relative locktime postpone must compare against creation time
# ------------------------------------------------------------------ #

def test_relative_locktime_postpone_uses_creation_time():
    """For relative locktimes the postpone check resolves the requested
    locktime against the will creation time (``w.time``), not against the
    current ``datetime.now()``.

    This also verifies that the comparison is effectively relative: the same
    relative string stays coherent even when wall-clock time advances, while a
    longer relative string is detected as a postpone."""
    now = int(time.time())
    ha = fx.heir_addresses(3)
    we_addr = fx.willexecutor_addresses(1)[0]
    wes = _willexecutors([("https://we.example", we_addr, 100000)])

    heirs_initial = {
        "alice": [ha[0], "40%", "300d"],
        "bob":   [ha[1], "30%", "300d"],
        "carol": [ha[2], "30%", "300d"],
    }
    h_initial = _make_heirs(heirs_initial)

    txs = _build(h_initial, wes, from_locktime=now)
    tx = _tx_for_we(txs, "https://we.example")
    txid = getattr(tx, "_test_txid", None) or f"tx{next(_TXID_COUNTER):08x}"
    item = _item_from_tx(tx, txid=txid, complete=True, pushed=True, time=now)

    # Advance wall-clock by two days.
    advanced_now_dt = datetime.fromtimestamp(now + 2 * DAY)
    import bal.core.plugin_base as pb_mod

    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 = BalTimestamp("30d").to_timestamp()

    # Same relative locktime after two days must remain coherent (no spurious
    # postpone detection).
    Will.check_willexecutors_and_heirs(
        {txid: item}, h_initial, wes, False, check_date, 100
    )

    # Moving the relative locktime from "300d" to "1y" is a real postpone and
    # must be detected using the creation-time base.
    heirs_postponed = _make_heirs({
        "alice": [ha[0], "40%", "1y"],
        "bob":   [ha[1], "30%", "1y"],
        "carol": [ha[2], "30%", "1y"],
    })
    with pytest.raises(WillPostponedException):
        Will.check_willexecutors_and_heirs(
            {txid: item}, heirs_postponed, wes, False, check_date, 100
        )


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-rules tests passed")
