# SPDX-License-Identifier: MIT
"""Exact half-open and staged-window boundary regressions."""
import copy
import datetime as dt
import json
import sys
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))
import conform


def iso(epoch):
    return dt.datetime.fromtimestamp(epoch, dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


class TimingBoundaryRegressions(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        vectors = json.loads((ROOT / "vectors/positive.json").read_text())["vectors"]
        cls.accountability = next(x["document"] for x in vectors if x["id"] == "P-ACCOUNTABILITY")
        cls.state = next(x["document"] for x in vectors if x["id"] == "P-STATE-PAUSED-EXIT")

    def test_staged_windows_must_finish_strictly_before_end(self):
        base = copy.deepcopy(self.accountability)
        end = int(base["activePeriodEnd"])
        e = base["enforcement"]
        total = sum(int(e[x]) for x in ("responseWindow", "challengeWindow", "decisionWindow"))
        for offset, accepted in ((-1, True), (0, False), (1, False)):
            candidate = copy.deepcopy(base)
            candidate["enforcement"]["requiredBy"] = str(end - total + offset)
            if accepted:
                conform.validate_document(candidate)
            else:
                with self.assertRaises(conform.Failure):
                    conform.validate_document(candidate)

    def _state_at(self, state, observed):
        candidate = copy.deepcopy(self.state)
        start = int(candidate["definition"]["activePeriodStart"])
        end = int(candidate["definition"]["activePeriodEnd"])
        candidate["observedAt"] = iso(observed)
        candidate["lockedPrincipal"] = "1000000" if state == "Active" else "0"
        candidate["refundablePrincipal"] = "0" if state == "Active" else "1000000"
        candidate["pullCredits"] = [] if state == "Active" else candidate["pullCredits"]
        candidate["exitAvailableAt"] = str(end if state != "Active" else observed)
        candidate["transitionHistory"] = [
            {"from": "Uninitialized", "to": "Proposed", "at": str(start - 4), "eventDigest": "0x" + "81" * 32},
            {"from": "Proposed", "to": "Accepted", "at": str(start - 3), "eventDigest": "0x" + "82" * 32},
            {"from": "Accepted", "to": "Funded", "at": str(start - 2), "eventDigest": "0x" + "83" * 32},
        ]
        if state == "Active":
            candidate["transitionHistory"].append(
                {"from": "Funded", "to": "Active", "at": str(start), "eventDigest": "0x" + "84" * 32}
            )
        else:
            candidate["transitionHistory"].append(
                {"from": "Funded", "to": "Expired", "at": str(end), "eventDigest": "0x" + "85" * 32}
            )
        candidate["state"] = state
        return candidate

    def test_active_period_end_minus_one_end_and_end_plus_one(self):
        end = int(self.state["definition"]["activePeriodEnd"])
        conform.validate_document(self._state_at("Active", end - 1))
        for observed in (end, end + 1):
            with self.assertRaises(conform.Failure):
                conform.validate_document(self._state_at("Active", observed))
        with self.assertRaises(conform.Failure):
            conform.validate_document(self._state_at("Expired", end - 1))
        conform.validate_document(self._state_at("Expired", end))
        conform.validate_document(self._state_at("Expired", end + 1))

    def _settlement_state(self, target, final_at):
        candidate = copy.deepcopy(self.state)
        start = int(candidate["definition"]["activePeriodStart"])
        candidate["observedAt"] = iso(final_at)
        candidate["refundablePrincipal"] = "0"
        candidate["pullCredits"] = []
        candidate["exitAvailableAt"] = str(final_at)
        candidate["lockedPrincipal"] = "0" if target == "Settled" else "1000000"
        candidate["principalLiability"] = "0" if target == "Settled" else "1000000"
        candidate["principalWithdrawn"] = "1000000" if target == "Settled" else "0"
        path = [
            ("Uninitialized", "Proposed", start - 4),
            ("Proposed", "Accepted", start - 3),
            ("Accepted", "Funded", start - 2),
            ("Funded", "Active", start),
        ]
        tails = {
            "SettlementRequested": [("Active", "SettlementRequested")],
            "Finalizable": [("Active", "SettlementRequested"), ("SettlementRequested", "Finalizable")],
            "Challenged": [("Active", "SettlementRequested"), ("SettlementRequested", "Challenged")],
            "SettlementFrozen": [
                ("Active", "SettlementRequested"),
                ("SettlementRequested", "Finalizable"),
                ("Finalizable", "SettlementFrozen"),
            ],
            "Settled": [
                ("Active", "SettlementRequested"),
                ("SettlementRequested", "Finalizable"),
                ("Finalizable", "SettlementFrozen"),
                ("SettlementFrozen", "Settled"),
            ],
        }[target]
        for index, (source, destination) in enumerate(tails):
            path.append((source, destination, final_at - len(tails) + index + 1))
        candidate["transitionHistory"] = [
            {"from": source, "to": destination, "at": str(at), "eventDigest": "0x" + f"{index + 1:02x}" * 32}
            for index, (source, destination, at) in enumerate(path)
        ]
        candidate["state"] = target
        return candidate

    def test_every_settlement_and_finalization_transition_obeys_exclusive_end(self):
        end = int(self.state["definition"]["activePeriodEnd"])
        targets = ("SettlementRequested", "Finalizable", "Challenged", "SettlementFrozen", "Settled")
        for target in targets:
            conform.validate_document(self._settlement_state(target, end - 1))
            for forbidden in (end, end + 1):
                with self.assertRaises(conform.Failure, msg=f"{target} at {forbidden}"):
                    conform.validate_document(self._settlement_state(target, forbidden))

    def test_source_valid_staged_expiry_edges_are_in_canonical_closure(self):
        mapping = json.loads((ROOT / "mappings/canonical-v3.json").read_text())
        snapshot = json.loads((ROOT / "interfaces/canonical-v3-interfaces.v1.json").read_text())
        required = {
            "SettlementRequested>Expired",
            "Finalizable>Expired",
            "SettlementFrozen>Expired",
        }
        self.assertEqual(len(mapping["stateMachine"]["transitions"]), 25)
        self.assertEqual(mapping["stateMachine"], snapshot["stateMachine"])
        self.assertTrue(required <= set(snapshot["stateMachine"]["transitions"]))
        end = int(self.state["definition"]["activePeriodEnd"])
        for prior in ("SettlementRequested", "Finalizable", "SettlementFrozen"):
            candidate = self._settlement_state(prior, end - 1)
            candidate["transitionHistory"].append(
                {"from": prior, "to": "Expired", "at": str(end), "eventDigest": "0x" + "ee" * 32}
            )
            candidate["state"] = "Expired"
            candidate["observedAt"] = iso(end)
            candidate["lockedPrincipal"] = "0"
            candidate["principalLiability"] = "1000000"
            candidate["principalWithdrawn"] = "0"
            candidate["refundablePrincipal"] = "1000000"
            candidate["pullCredits"] = copy.deepcopy(self.state["pullCredits"])
            candidate["exitAvailableAt"] = str(end)
            conform.validate_document(candidate)

    def _accountability_timeout_state(self, breach_still_exists):
        candidate = copy.deepcopy(self.state)
        definition = copy.deepcopy(self.accountability)
        start = int(definition["activePeriodStart"])
        end = int(definition["activePeriodEnd"])
        candidate["kind"] = "accountability"
        candidate["bondId"] = definition["bondId"]
        candidate["definition"] = definition
        candidate["definitionDigest"] = conform.digest(definition)
        candidate["paused"] = True
        candidate["observedAt"] = iso(end)
        prefix = [
            ("Uninitialized", "Proposed", start - 4, "ordinary"),
            ("Proposed", "Accepted", start - 3, "ordinary"),
            ("Accepted", "Funded", start - 2, "ordinary"),
            ("Funded", "Active", start, "ordinary"),
            ("Active", "SettlementRequested", end - 1, "ordinary"),
        ]
        if breach_still_exists:
            tail = [
                ("SettlementRequested", "Finalizable", end, "accepted-liability-timeout"),
                ("Finalizable", "SettlementFrozen", end, "accepted-liability-timeout"),
                ("SettlementFrozen", "Settled", end, "accepted-liability-timeout"),
            ]
            candidate["state"] = "Settled"
            candidate["slashedPrincipal"] = "100000"
            candidate["refundablePrincipal"] = "900000"
            candidate["lockedPrincipal"] = "0"
            candidate["principalLiability"] = "1000000"
            candidate["principalWithdrawn"] = "0"
            candidate["pullCredits"] = [{"holder": definition["aiCompany"], "amount": "900000"}]
            outcome = "settled-liability"
        else:
            tail = [("SettlementRequested", "Cancelled", end, "timeout-refund")]
            candidate["state"] = "Cancelled"
            candidate["slashedPrincipal"] = "0"
            candidate["refundablePrincipal"] = "1000000"
            candidate["lockedPrincipal"] = "0"
            candidate["principalLiability"] = "1000000"
            candidate["principalWithdrawn"] = "0"
            candidate["pullCredits"] = [{"holder": definition["aiCompany"], "amount": "1000000"}]
            outcome = "full-refund"
        candidate["transitionHistory"] = [
            {"from": source, "to": destination, "at": str(at), "mode": mode, "eventDigest": "0x" + f"{index + 10:02x}" * 32}
            for index, (source, destination, at, mode) in enumerate(prefix + tail)
        ]
        candidate["timeoutResolution"] = {
            "acceptedEvidenceAt": str(end - 1),
            "canonicalRevalidatedAt": str(end),
            "sameTimestampRevalidation": True,
            "canonicalBreachStillExists": breach_still_exists,
            "outcome": outcome,
        }
        candidate["exitAvailableAt"] = str(end)
        return candidate

    def test_same_timestamp_pause_safe_accepted_liability_timeout(self):
        settled = self._accountability_timeout_state(True)
        conform.validate_document(settled)
        refunded = self._accountability_timeout_state(False)
        conform.validate_document(refunded)
        for mutate in (
            lambda d: d["timeoutResolution"].update(canonicalRevalidatedAt=str(int(d["timeoutResolution"]["canonicalRevalidatedAt"]) + 1)),
            lambda d: d["timeoutResolution"].update(canonicalBreachStillExists=False),
            lambda d: d["transitionHistory"][-1].update(mode="ordinary"),
        ):
            candidate = copy.deepcopy(settled)
            mutate(candidate)
            with self.assertRaises(conform.Failure):
                conform.validate_document(candidate)

    def test_final_accountability_abi_excludes_retired_dispute_selectors(self):
        mapping = json.loads((ROOT / "mappings/canonical-v3.json").read_text())
        abi = next(x for x in mapping["sdkDeployments"] if x["sdkName"] == "accountabilityBond")
        retired = {
            "advanceToFinalizable(uint256)", "attestBreach(uint256,uint256,bytes32,uint96)",
            "breachSnapshot(uint256,uint256,bytes32)", "cancelStalledSettlement(uint256)",
            "challengeSettlement(uint256,uint256)", "disputeSubject(uint256)",
            "freezeSettlement(uint256,uint96)", "releaseDisputePayout(uint256,address,uint256)",
            "resolveChallenge(uint256)", "settle(uint256)",
        }
        self.assertEqual(abi["signatureCount"], 36)
        self.assertEqual(abi["abiIdentity"], "0x0c18d05c868716fad4bfc1fcfad2da51c83f247de06a7e3871dcece768a9526f")
        self.assertFalse(retired.intersection(abi["canonicalSignatures"]))


if __name__ == "__main__":
    unittest.main()
