"""
Comprehensive unit tests for ``bal.core.util.Util``.

Covers every static method — locktime helpers, amount helpers, comparison
helpers, UTXO helpers, and migration helpers — with edge cases.

Run:
    source electrum/env/bin/activate
    python3 tests/test_core_util.py
"""

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

from bal.core.util import Util


def test_locktime_to_str():
    # timestamp -> ISO format
    s = Util.locktime_to_str(1700000000)
    assert "202" in s and "-" in s, f"expected ISO string, got {s!r}"

    # small value -> still treated as timestamp
    s = Util.locktime_to_str(500000)
    assert "-" in s and ":" in s, f"expected ISO string, got {s!r}"

    # string input -> unchanged
    assert Util.locktime_to_str("hello") == "hello"

    # zero -> still treated as timestamp
    s = Util.locktime_to_str(0)
    assert "-" in s, f"expected ISO date, got {s!r}"


def test_str_to_locktime():
    # relative suffixes pass through
    assert Util.str_to_locktime("30d") == "30d"
    assert Util.str_to_locktime("1y") == "1y"

    # integer string -> int
    assert isinstance(Util.str_to_locktime("500000"), int)
    assert Util.str_to_locktime("500000") == 500000

    # ISO date -> int timestamp
    ts = Util.str_to_locktime("2025-01-01T00:00:00")
    assert isinstance(ts, int)
    assert ts > 0


def test_parse_locktime_string():
    # plain int -> same int
    assert Util.parse_locktime_string(500000) == 500000

    # int as string -> int
    assert Util.parse_locktime_string("500000") == 500000

    # relative days -> > current timestamp
    result = Util.parse_locktime_string("7d")
    import time
    assert result > time.time() - 86400

    # relative years -> > current timestamp
    result = Util.parse_locktime_string("1y")
    assert result > time.time()

    # invalid -> 0
    assert Util.parse_locktime_string("") == 0
    assert Util.parse_locktime_string("garbage") == 0


def test_int_locktime():
    assert Util.int_locktime(seconds=1) == 1
    assert Util.int_locktime(minutes=1) == 60
    assert Util.int_locktime(hours=1) == 3600
    assert Util.int_locktime(days=1) == 86400
    assert Util.int_locktime() == 0


def test_encode_decode_amount():
    dp = 8  # typical BTC decimal point

    # percentage passes through
    assert Util.encode_amount("50%", dp) == "50%"
    assert Util.decode_amount("50%", dp) == "50%"

    # satoshi encoding
    assert Util.encode_amount("1.0", dp) == 100000000
    assert Util.encode_amount("0.5", dp) == 50000000

    # decoding
    assert Util.decode_amount(100000000, dp) == "1.00000000"
    assert Util.decode_amount(50000000, dp) == "0.50000000"

    # edge
    assert Util.encode_amount("abc", dp) == 0
    assert Util.decode_amount("abc", dp) == "abc"


def test_is_perc():
    assert Util.is_perc("50%") is True
    assert Util.is_perc("100%") is True
    assert Util.is_perc("0%") is True
    assert Util.is_perc("100") is False
    assert Util.is_perc(50) is False
    assert Util.is_perc("") is False
    assert Util.is_perc(None) is False


def test_cmp_array():
    assert Util.cmp_array([1, 2, 3], [1, 2, 3]) is True
    assert Util.cmp_array([1, 2, 3], [1, 2]) is False
    assert Util.cmp_array([], []) is True
    assert Util.cmp_array([1], [2]) is False
    assert Util.cmp_array(None, None) is False  # exception path


def test_cmp_heir():
    heira = ["abc", 10000, 12345]
    heirb = ["abc", 10000, 54321]
    assert Util.cmp_heir(heira, heirb) is True  # addr(0) + amount(1) match

    heirb2 = ["xyz", 10000, 12345]
    assert Util.cmp_heir(heira, heirb2) is False  # addr mismatch

    heirb3 = ["abc", 20000, 12345]
    assert Util.cmp_heir(heira, heirb3) is False  # amount mismatch


def test_cmp_willexecutor():
    a = {"url": "https://we.example", "address": "bc1abc", "base_fee": 1000}
    b = {"url": "https://we.example", "address": "bc1abc", "base_fee": 1000}
    assert Util.cmp_willexecutor(a, b) is True

    c = {"url": "https://we.other", "address": "bc1abc", "base_fee": 1000}
    assert Util.cmp_willexecutor(a, c) is False

    assert Util.cmp_willexecutor(None, None) is True  # None == None
    assert Util.cmp_willexecutor({}, {}) is True  # both empty


def test_search_heir_by_values():
    heirs = {
        "alice": {0: "addr1", 1: 1000, 3: 500},
        "bob": {0: "addr2", 1: 2000, 3: 600},
    }
    match = Util.search_heir_by_values(heirs, {0: "addr1", 3: 500}, [0, 3])
    assert match == "alice"

    no_match = Util.search_heir_by_values(heirs, {0: "addrX", 3: 500}, [0, 3])
    assert no_match is False

    assert Util.search_heir_by_values({}, {0: "x"}, [0]) is False


def test_cmp_heir_by_values():
    a = {0: "addr1", 1: 1000, 3: 500}
    b = {0: "addr1", 1: 1000, 3: 500}
    assert Util.cmp_heir_by_values(a, b, [0, 1]) is True
    assert Util.cmp_heir_by_values(a, b, [0, 1, 3]) is True

    c = {0: "addr1", 1: 9999, 3: 500}
    assert Util.cmp_heir_by_values(a, c, [1]) is False


def test_cmp_heirs_by_values():
    a = {"h1": {0: "a1", 1: 100}, "h2": {0: "a2", 1: 200}}
    b = {"h3": {0: "a1", 1: 100}, "h4": {0: "a2", 1: 200}}
    assert Util.cmp_heirs_by_values(a, b, [0, 1]) is True

    c = {"h1": {0: "aX", 1: 100}}
    assert Util.cmp_heirs_by_values(a, c, [0, 1]) is False


def test_cmp_inputs():
    # Without real TxInput objects we test edge cases
    assert Util.cmp_inputs([], []) is True
    assert Util.cmp_inputs([1], []) is False
    assert Util.cmp_inputs([], [1]) is False


def test_cmp_outputs():
    assert Util.cmp_outputs([], []) is True
    assert Util.cmp_outputs([1], []) is False
    assert Util.cmp_outputs([], [1]) is False


def test_cmp_txs():
    # No real Transaction objects, but edge coverage
    class FakeTx:
        def inputs(self): return []
        def outputs(self): return []
    a = FakeTx()
    assert Util.cmp_txs(a, a) is True


def test_get_value_amount():
    class FakeOutput:
        def __init__(self, addr, val):
            self.address = addr
            self.value = val

    class FakeTx:
        def outputs(self): return self._outs
        def __init__(self, outs): self._outs = outs

    # Shared addr+value → both same_amount and same_address → value counted
    out_a = FakeOutput("bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", 1000)
    out_b = FakeOutput("bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", 1000)
    result = Util.get_value_amount(FakeTx([out_a]), FakeTx([out_b]))
    assert result == 1000, f"expected 1000, got {result}"

    # Different address, same amount → same_amount only → not counted
    out_c = FakeOutput("bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", 1000)
    result2 = Util.get_value_amount(FakeTx([out_a]), FakeTx([out_c]))
    assert result2 == 0, f"expected 0, got {result2}"

    # No matching amount → returns False
    out_d = FakeOutput("bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", 999)
    result3 = Util.get_value_amount(FakeTx([out_a]), FakeTx([out_d]))
    assert result3 is False, f"expected False, got {result3}"


def test_chk_locktime():
    now_ts = 1700000000

    # timestamp locktime still in future
    assert Util.chk_locktime(now_ts, 1800000000) is True

    # timestamp locktime in past
    assert Util.chk_locktime(now_ts, 1000000000) is False

    # locktime at boundary
    assert Util.chk_locktime(now_ts, now_ts) is False


def test_anticipate_locktime():
    ts = 1700000000
    result = Util.anticipate_locktime(ts, days=1)
    assert result < ts
    assert result > 0

    # overflow handling (Windows-safe)
    huge = 2**32 - 1  # NLOCKTIME_MAX
    result = Util.anticipate_locktime(huge, days=1)
    assert result > 0

    # clamp to minimum 1
    low = Util.anticipate_locktime(10, days=20)
    assert low >= 1


def test_cmp_locktime():
    assert Util.cmp_locktime("30d", "30d") == 0
    # Note: cmp_locktime may return nonzero or None for mismatched units


def test_get_locktimes():
    class FakeTx:
        locktime = 1700000000

    # will with single entry
    will = {
        "tx1": {"tx": FakeTx()},
    }
    locktimes = list(Util.get_locktimes(will))
    assert 1700000000 in locktimes
    assert len(locktimes) == 1

    # empty will
    assert list(Util.get_locktimes({})) == []


def test_get_lowest_locktimes():
    result = Util.get_lowest_locktimes([500000, 1700000000, 100, 900000])
    assert isinstance(result, list)
    assert result == sorted(result)
    assert 100 in result
    assert 1700000000 in result

    # empty
    assert Util.get_lowest_locktimes([]) == []


def test_get_will_spent_utxos():
    class FakeTx:
        def inputs(self): return [1, 2, 3]

    will = {
        "tx1": {"tx": FakeTx()},
        "tx2": {"tx": FakeTx()},
    }
    utxos = Util.get_will_spent_utxos(will)
    assert len(utxos) == 6  # 3 inputs * 2 txs


def test_utxo_to_str():
    class FakeUtxo:
        def to_str(self): return "txid:0"
    assert Util.utxo_to_str(FakeUtxo()) == "txid:0"

    class FakePrevout:
        def to_str(self): return "txid:1"
    class FakeUtxo2:
        to_str = None
        prevout = FakePrevout()
    assert Util.utxo_to_str(FakeUtxo2()) == "txid:1"

    # fallback
    class Broken:
        pass
    assert len(Util.utxo_to_str(Broken())) > 0


def test_cmp_utxo():
    class A:
        def to_str(self): return "abc:0"
    assert Util.cmp_utxo(A(), A()) is True

    class B:
        def to_str(self): return "xyz:1"
    assert Util.cmp_utxo(A(), B()) is False


def test_in_utxo():
    class U:
        def __init__(self, s):
            self._s = s
        def to_str(self): return self._s

    utxos = [U("a:0"), U("b:1")]
    target = U("a:0")
    assert Util.in_utxo(target, utxos) is True
    assert Util.in_utxo(U("z:9"), utxos) is False
    assert Util.in_utxo(target, []) is False


def test_cmp_output():
    class O:
        def __init__(self, addr, val):
            self.address = addr
            self.value = val
    assert Util.cmp_output(O("a", 100), O("a", 100)) is True
    assert Util.cmp_output(O("a", 100), O("b", 100)) is False
    assert Util.cmp_output(O("a", 100), O("a", 200)) is False


def test_in_output():
    class O:
        def __init__(self, addr, val):
            self.address = addr
            self.value = val
    outputs = [O("a", 100), O("b", 200)]
    assert Util.in_output(O("a", 100), outputs) is True
    assert Util.in_output(O("z", 999), outputs) is False
    assert Util.in_output(O("a", 100), []) is False


def test_din_output():
    class O:
        def __init__(self, addr, val):
            self.address = addr
            self.value = val

    outputs = [O("a", 100), O("b", 200)]

    # same amount AND same address
    same_amt, same_addr = Util.din_output(O("a", 100), outputs)
    assert same_amt is True and same_addr is True

    # same amount but different address
    same_amt, same_addr = Util.din_output(O("c", 100), outputs)
    assert same_amt is True and same_addr is False

    # different amount
    same_amt, same_addr = Util.din_output(O("z", 999), outputs)
    assert same_amt is False and same_addr is False


def test_get_current_height():
    # returns current UNIX timestamp, ignoring network
    import time
    now = Util.get_current_height(None)
    assert abs(now - int(time.time())) < 5


def test_copy():
    d = {"a": 1}
    Util.copy(d, {"b": 2})
    assert d == {"a": 1, "b": 2}

    # overwrite
    Util.copy(d, {"a": 99})
    assert d["a"] == 99


def test_fix_will_settings_tx_fees():
    settings = {"tx_fees": 50}
    assert Util.fix_will_settings_tx_fees(settings) is True
    assert settings["baltx_fees"] == 50
    assert "tx_fees" not in settings

    # no migration needed
    assert Util.fix_will_settings_tx_fees({}) is False


def test_fix_will_tx_fees():
    will = {
        "tx1": {"tx_fees": 30},
        "tx2": {"baltx_fees": 50},
    }
    assert Util.fix_will_tx_fees(will) is True
    assert will["tx1"]["baltx_fees"] == 30
    assert "tx_fees" not in will["tx1"]

    # empty will
    assert Util.fix_will_tx_fees({}) is False


def test_text_hex_conversion():
    assert Util.text_to_hex("BAL") == "42414c"
    assert Util.hex_to_text("42414c") == "BAL"
    assert Util.text_to_hex("") == ""
    assert Util.hex_to_text("") == ""
    assert Util.hex_to_text("ZZZ") == "Error: Invalid hex string"


if __name__ == "__main__":
    test_locktime_to_str()
    test_str_to_locktime()
    test_parse_locktime_string()
    test_int_locktime()
    test_encode_decode_amount()
    test_is_perc()
    test_cmp_array()
    test_cmp_heir()
    test_cmp_willexecutor()
    test_search_heir_by_values()
    test_cmp_heir_by_values()
    test_cmp_heirs_by_values()
    test_cmp_inputs()
    test_cmp_outputs()
    test_cmp_txs()
    test_get_value_amount()
    test_chk_locktime()
    test_anticipate_locktime()
    test_cmp_locktime()
    test_get_locktimes()
    test_get_lowest_locktimes()
    test_get_will_spent_utxos()
    test_utxo_to_str()
    test_cmp_utxo()
    test_in_utxo()
    test_cmp_output()
    test_in_output()
    test_din_output()
    test_get_current_height()
    test_copy()
    test_fix_will_settings_tx_fees()
    test_fix_will_tx_fees()
    test_text_hex_conversion()
    print(f"[OK] All {sum(1 for k in dir() if k.startswith('test_'))} Util tests passed")
