#!/usr/bin/env python3
"""Regression checks for the unsigned-only Sepolia rehearsal tool."""
from __future__ import annotations

import json
import importlib.util
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "base_sepolia_rehearsal.py"
INTAKE = ROOT / "templates" / "public-address-intake.template.json"
MODULE_SPEC = importlib.util.spec_from_file_location("base_sepolia_rehearsal", SCRIPT)
assert MODULE_SPEC and MODULE_SPEC.loader
MODULE = importlib.util.module_from_spec(MODULE_SPEC)
MODULE_SPEC.loader.exec_module(MODULE)


class BaseSepoliaRehearsalTest(unittest.TestCase):
    def test_rpc_check_identifies_the_read_only_client(self):
        class Response:
            def __enter__(self):
                return self

            def __exit__(self, *_args):
                return False

            def read(self):
                return b'{"jsonrpc":"2.0","id":1,"result":"0x14a34"}'

        def fake_urlopen(request, timeout):
            self.assertEqual(timeout, 10)
            self.assertEqual(request.get_header("User-agent"), "Vaultfire-Base-Sepolia-Rehearsal/1.0")
            return Response()

        with mock.patch.object(MODULE.urllib.request, "urlopen", side_effect=fake_urlopen):
            result = MODULE.rpc_check("https://sepolia.base.org")

        self.assertTrue(result["reachable"])
        self.assertEqual(result["chainId"], 84532)
        self.assertEqual(result["errors"], [])

    def run_tool(self, *args: str) -> subprocess.CompletedProcess[str]:
        return subprocess.run([sys.executable, str(SCRIPT), *args], text=True, capture_output=True, check=False)

    def test_incomplete_template_renders_unsigned_plan_and_closed_gate(self) -> None:
        plan = self.run_tool("plan", "--intake", str(INTAKE))
        self.assertEqual(plan.returncode, 0, plan.stderr)
        rendered = json.loads(plan.stdout)
        self.assertEqual(rendered["mode"], "UNSIGNED_PLAN_ONLY")
        self.assertTrue(rendered["broadcastProhibited"])
        self.assertTrue(rendered["failClosed"])
        self.assertEqual(len(rendered["steps"]), 19)

        gate = self.run_tool("gate", "--intake", str(INTAKE))
        self.assertEqual(gate.returncode, 2)
        self.assertFalse(json.loads(gate.stdout)["publicPreflightPassed"])

    def test_rejects_secret_bearing_input(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            unsafe = Path(temporary) / "unsafe.json"
            payload = json.loads(INTAKE.read_text())
            payload["privateKey"] = "0x" + "11" * 32
            unsafe.write_text(json.dumps(payload))
            result = self.run_tool("plan", "--intake", str(unsafe))
            self.assertEqual(result.returncode, 0)
            blockers = json.loads(result.stdout)["blockers"]
            self.assertTrue(any("forbidden secret-bearing field" in blocker for blocker in blockers))

    def test_generated_record_template_has_complete_scenario_register(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            record = Path(temporary) / "record.json"
            made = self.run_tool("record-template", "--out", str(record))
            self.assertEqual(made.returncode, 0, made.stderr)
            payload = json.loads(record.read_text())
            self.assertEqual(len(payload["scenarios"]), 19)
            checked = self.run_tool("validate-record", "--record", str(record))
            self.assertEqual(checked.returncode, 2)
            result = json.loads(checked.stdout)
            self.assertTrue(result["recordStructurallyValid"])
            self.assertFalse(result["rehearsalComplete"])
            self.assertTrue(result["failClosed"])

    def test_reconciliation_fails_when_liabilities_exceed_assets(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            record = Path(temporary) / "record.json"
            self.run_tool("record-template", "--out", str(record))
            payload = json.loads(record.read_text())
            payload["reconciliations"][0]["observedBalanceAtomic"] = "1"
            payload["reconciliations"][0]["principalLiabilityAtomic"] = "2"
            record.write_text(json.dumps(payload))
            checked = self.run_tool("validate-record", "--record", str(record))
            self.assertEqual(checked.returncode, 2)
            self.assertTrue(json.loads(checked.stdout)["failClosed"])

    def test_observed_transaction_hash_and_required_events_are_recordable(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            record = Path(temporary) / "record.json"
            self.run_tool("record-template", "--out", str(record))
            payload = json.loads(record.read_text())
            scenario = payload["scenarios"][0]
            scenario["status"] = "observed"
            scenario["transactionHashes"] = ["0x" + "ab" * 32]
            scenario["actualState"] = {"proposalId": "1", "executed": True}
            scenario["events"] = [
                {"name": "ProposedV3", "emitter": "0x0000000000000000000000000000000000000001", "logIndex": "0", "blockNumber": "100"},
                {"name": "ConfirmedV3", "emitter": "0x0000000000000000000000000000000000000001", "logIndex": "1", "blockNumber": "101"},
                {"name": "ExecutedV3", "emitter": "0x0000000000000000000000000000000000000001", "logIndex": "2", "blockNumber": "102"},
            ]
            payload["contracts"] = {key: "0x0000000000000000000000000000000000000001" for key in payload["contracts"]}
            record.write_text(json.dumps(payload))
            checked = self.run_tool("validate-record", "--record", str(record))
            self.assertEqual(checked.returncode, 2)
            result = json.loads(checked.stdout)
            self.assertTrue(result["recordStructurallyValid"])
            self.assertIn("GOV-02", result["incompleteScenarios"])

    def test_complete_public_record_can_pass_without_secret_material(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            record = Path(temporary) / "record.json"
            self.run_tool("record-template", "--out", str(record))
            payload = json.loads(record.read_text())
            import importlib.util
            spec = importlib.util.spec_from_file_location("harness", SCRIPT)
            harness = importlib.util.module_from_spec(spec)
            assert spec.loader is not None
            spec.loader.exec_module(harness)
            required_events = {scenario_id: names for scenario_id, _, names in harness.SCENARIOS}
            for index, scenario in enumerate(payload["scenarios"]):
                scenario["status"] = "observed"
                scenario["transactionHashes"] = ["0x" + f"{index + 1:064x}"]
                scenario["actualState"] = {"result": "recorded"}
                scenario["events"] = [
                    {
                        "name": event,
                        "emitter": "0x0000000000000000000000000000000000000001",
                        "logIndex": str(event_index),
                        "blockNumber": "100",
                    }
                    for event_index, event in enumerate(required_events[scenario["id"]])
                ]
            payload["contracts"] = {key: "0x0000000000000000000000000000000000000001" for key in payload["contracts"]}
            record.write_text(json.dumps(payload))
            checked = self.run_tool("validate-record", "--record", str(record))
            self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
            self.assertTrue(json.loads(checked.stdout)["rehearsalPassed"])

    def test_duplicate_observed_scenario_fails_even_when_every_id_is_present(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            record = Path(temporary) / "record.json"
            self.run_tool("record-template", "--out", str(record))
            payload = json.loads(record.read_text())
            required_events = {scenario_id: names for scenario_id, _, names in MODULE.SCENARIOS}
            for index, scenario in enumerate(payload["scenarios"]):
                scenario["status"] = "observed"
                scenario["transactionHashes"] = ["0x" + f"{index + 1:064x}"]
                scenario["actualState"] = {"result": "recorded"}
                scenario["events"] = [
                    {
                        "name": event,
                        "emitter": "0x0000000000000000000000000000000000000001",
                        "logIndex": str(event_index),
                        "blockNumber": "100",
                    }
                    for event_index, event in enumerate(required_events[scenario["id"]])
                ]
            payload["scenarios"].append(dict(payload["scenarios"][0]))
            payload["contracts"] = {key: "0x0000000000000000000000000000000000000001" for key in payload["contracts"]}
            record.write_text(json.dumps(payload))
            checked = self.run_tool("validate-record", "--record", str(record))
            result = json.loads(checked.stdout)
            self.assertEqual(checked.returncode, 2)
            self.assertFalse(result["recordStructurallyValid"])
            self.assertFalse(result["rehearsalPassed"])
            self.assertTrue(any("exactly one entry" in error for error in result["errors"]))


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


class DuplicateJsonInputTest(unittest.TestCase):
    def test_rehearsal_loader_rejects_root_and_nested_duplicate_members(self):
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory) / "input.json"
            path.write_text('{"schema":"a","schema":"b"}', encoding="utf-8")
            with self.assertRaisesRegex(ValueError, "duplicate"):
                MODULE.load_json(path)
            path.write_text('{"network":{"chainId":84532,"chainId":1}}', encoding="utf-8")
            with self.assertRaisesRegex(ValueError, "duplicate"):
                MODULE.load_json(path)
