#!/usr/bin/env python3
"""Standard-library edge tests for lease-broker atomic state persistence."""

from __future__ import annotations

import copy
import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch


DAEMON_PATH = Path(__file__).parents[2] / "framework/tools/lease-broker/daemon.py"
SPEC = importlib.util.spec_from_file_location("lease_broker_daemon", DAEMON_PATH)
if SPEC is None or SPEC.loader is None:
    raise RuntimeError("unable to load lease broker daemon")
DAEMON = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(DAEMON)


class StateStoreCommitTest(unittest.TestCase):
    def make_store(self, root: Path):
        os.chmod(root, 0o700)
        store = DAEMON.StateStore(root / "state.json")
        store.value["marker"] = "partial-write-proof"
        return store

    def test_partial_writes_persist_the_complete_payload(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            store = self.make_store(root)
            real_write = os.write

            def partial_write(descriptor: int, payload: bytes) -> int:
                return real_write(descriptor, payload[: max(1, len(payload) // 3)])

            with patch.object(DAEMON.os, "write", side_effect=partial_write):
                store.commit()

            self.assertEqual(json.loads(store.path.read_text()), store.value)

    def test_zero_progress_removes_owned_temporary_file(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            store = self.make_store(root)
            with patch.object(DAEMON.os, "write", return_value=0):
                with self.assertRaises(OSError):
                    store.commit()

            self.assertFalse(store.path.exists())
            self.assertEqual(list(root.glob(".*.tmp")), [])

    def test_oversized_payload_is_refused_before_replacing_state(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            store = self.make_store(root)
            store.value.pop("marker")
            store.commit()
            durable = store.path.read_bytes()
            store.value["oversized"] = "x" * DAEMON.MAX_STATE

            with patch.object(DAEMON.os, "open", wraps=os.open) as mocked_open:
                with self.assertRaisesRegex(DAEMON.BrokerFailure, "STATE_TOO_LARGE"):
                    store.commit()

            self.assertEqual(mocked_open.call_count, 0)
            self.assertEqual(store.path.read_bytes(), durable)
            self.assertEqual(list(root.glob(".*.tmp")), [])


class StateStoreValidationTest(unittest.TestCase):
    @staticmethod
    def binding() -> dict[str, object]:
        return {
            "compaction_epoch": 0,
            "request_epoch": 0,
            "h_source": "a" * 64,
            "h_payload": "b" * 64,
            "schema_version": 1,
        }

    def test_impossible_or_over_capacity_token_state_is_rejected(self) -> None:
        session_id = "1" * 64
        session = {
            "anchor_pid": 123,
            "anchor_starttime": "456",
            "runtime_generation": 2,
        }
        live_token = {
            "session_id": session_id,
            "runtime_generation": 2,
            "binding": self.binding(),
            "consumed": False,
        }
        cases = {
            "stale generation": {
                "2" * 64: {**live_token, "runtime_generation": 1},
            },
            "consumed token": {
                "2" * 64: {**live_token, "consumed": True},
            },
            "over capacity": {
                f"{index:064x}": copy.deepcopy(live_token)
                for index in range(DAEMON.MAX_PENDING_TOKENS + 1)
            },
        }

        for label, tokens in cases.items():
            with self.subTest(label=label), tempfile.TemporaryDirectory() as directory:
                root = Path(directory)
                os.chmod(root, 0o700)
                state_path = root / "state.json"
                state_path.write_text(json.dumps({
                    "version": 1,
                    "sessions": {session_id: session},
                    "tokens": tokens,
                }))
                os.chmod(state_path, 0o600)

                with self.assertRaisesRegex(DAEMON.BrokerFailure, "STATE_INTEGRITY"):
                    DAEMON.StateStore(state_path)


class BrokerBehaviorTest(unittest.TestCase):
    def make_broker(self, root: Path):
        os.chmod(root, 0o700)
        return DAEMON.Broker(DAEMON.StateStore(root / "state.json"))

    @staticmethod
    def binding() -> dict[str, object]:
        return {
            "compaction_epoch": 0,
            "request_epoch": 0,
            "h_source": "a" * 64,
            "h_payload": "b" * 64,
            "schema_version": 1,
        }

    def register(self, broker, generation: int = 1) -> str:
        response = broker.handle((123, 1000, 1000), {
            "action": "register_anchor",
            "runtime_generation": generation,
        })
        return response["session_id"]

    def mint(self, broker, session_id: str, generation: int = 1) -> str:
        response = broker.handle((123, 1000, 1000), {
            "action": "mint_token",
            "session_id": session_id,
            "runtime_generation": generation,
            "binding": self.binding(),
        })
        return response["token"]

    def consume(self, broker, session_id: str, token: str, generation: int = 1):
        return broker.handle((123, 1000, 1000), {
            "action": "consume_token",
            "session_id": session_id,
            "runtime_generation": generation,
            "token": token,
        })

    def test_anchor_generation_bump_reuses_session_and_revokes_token(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            broker = self.make_broker(root)
            with (
                patch.object(
                    DAEMON,
                    "proc_node",
                    return_value={"pid": 123, "ppid": 1, "starttime": "456"},
                ),
                patch.object(DAEMON, "verified_ancestry", return_value=True),
            ):
                first = broker.handle((123, 1000, 1000), {
                    "action": "register_anchor",
                    "runtime_generation": 1,
                })
                minted = broker.handle((123, 1000, 1000), {
                    "action": "mint_token",
                    "session_id": first["session_id"],
                    "runtime_generation": 1,
                    "binding": self.binding(),
                })
                bumped = broker.handle((123, 1000, 1000), {
                    "action": "register_anchor",
                    "runtime_generation": 2,
                })
                repeated = broker.handle((123, 1000, 1000), {
                    "action": "register_anchor",
                    "runtime_generation": 2,
                })

            self.assertEqual(bumped["session_id"], first["session_id"])
            self.assertEqual(repeated["session_id"], first["session_id"])
            self.assertNotIn(minted["token"], broker.store.tokens())
            restarted = self.make_broker(root)
            self.assertEqual(restarted.store.tokens(), {})
            self.assertEqual(
                restarted.store.sessions()[first["session_id"]]["runtime_generation"], 2
            )
            with self.assertRaisesRegex(DAEMON.BrokerFailure, "STALE_GENERATION"):
                with patch.object(DAEMON, "proc_node", return_value={"starttime": "456"}):
                    broker.handle((123, 1000, 1000), {
                        "action": "register_anchor",
                        "runtime_generation": 1,
                    })

    def test_successful_consume_deletes_token_and_replay_is_refused(self) -> None:
        with tempfile.TemporaryDirectory() as directory, (
            patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
        ), patch.object(DAEMON, "verified_ancestry", return_value=True):
            broker = self.make_broker(Path(directory))
            session_id = self.register(broker)
            token = self.mint(broker, session_id)

            self.assertEqual(self.consume(broker, session_id, token), {"ok": True})
            self.assertNotIn(token, broker.store.tokens())
            with self.assertRaisesRegex(DAEMON.BrokerFailure, "TOKEN_REPLAY"):
                self.consume(broker, session_id, token)

    def test_normal_cycles_remain_bounded_and_restartable(self) -> None:
        with tempfile.TemporaryDirectory() as directory, (
            patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
        ), patch.object(DAEMON, "verified_ancestry", return_value=True):
            root = Path(directory)
            broker = self.make_broker(root)
            session_id = self.register(broker)

            for _ in range(DAEMON.MAX_PENDING_TOKENS * 3):
                self.consume(broker, session_id, self.mint(broker, session_id))

            self.assertEqual(broker.store.tokens(), {})
            self.assertLess((root / "state.json").stat().st_size, DAEMON.MAX_STATE)
            restarted = self.make_broker(root)
            self.assertEqual(restarted.store.tokens(), {})
            self.assertIn(session_id, restarted.store.sessions())

    def test_pending_token_capacity_refusal_does_not_mutate_state(self) -> None:
        with tempfile.TemporaryDirectory() as directory, (
            patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
        ), patch.object(DAEMON, "verified_ancestry", return_value=True):
            root = Path(directory)
            broker = self.make_broker(root)
            session_id = self.register(broker)
            for _ in range(DAEMON.MAX_PENDING_TOKENS):
                self.mint(broker, session_id)
            before = copy.deepcopy(broker.store.value)
            durable = broker.store.path.read_bytes()

            with self.assertRaisesRegex(DAEMON.BrokerFailure, "TOKEN_CAPACITY"):
                self.mint(broker, session_id)

            self.assertEqual(broker.store.value, before)
            self.assertEqual(broker.store.path.read_bytes(), durable)

    def test_directory_fsync_failure_poisoned_store_cannot_continue(self) -> None:
        with tempfile.TemporaryDirectory() as directory, patch.object(
            DAEMON,
            "proc_node",
            return_value={"pid": 123, "ppid": 1, "starttime": "456"},
        ):
            root = Path(directory)
            broker = self.make_broker(root)
            real_fsync = os.fsync

            def fail_directory_fsync(descriptor: int) -> None:
                if os.path.isdir(f"/proc/self/fd/{descriptor}"):
                    raise OSError("directory fsync failed")
                real_fsync(descriptor)

            with patch.object(DAEMON.os, "fsync", side_effect=fail_directory_fsync):
                with self.assertRaisesRegex(
                    DAEMON.StateCommitUncertain, "STATE_COMMIT_UNCERTAIN"
                ):
                    self.register(broker)

            durable = json.loads(broker.store.path.read_text())
            self.assertEqual(broker.store.value, durable)
            self.assertTrue(broker.store.poisoned)
            before = copy.deepcopy(broker.store.value)
            with self.assertRaisesRegex(
                DAEMON.StateCommitUncertain, "STATE_COMMIT_UNCERTAIN"
            ):
                broker.handle((123, 1000, 1000), {
                    "action": "register_anchor",
                    "runtime_generation": 2,
                })
            self.assertEqual(broker.store.value, before)

    def test_commit_failures_before_replace_roll_back_every_broker_mutation(self) -> None:
        with tempfile.TemporaryDirectory() as directory, (
            patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
        ), patch.object(DAEMON, "verified_ancestry", return_value=True):
            root = Path(directory)
            broker = self.make_broker(root)
            session_id = self.register(broker)
            token = self.mint(broker, session_id)

            def assert_rollback(request: dict[str, object]) -> None:
                before = copy.deepcopy(broker.store.value)
                durable = broker.store.path.read_bytes()
                with patch.object(broker.store, "commit", side_effect=OSError("fsync failed")):
                    with self.assertRaisesRegex(OSError, "fsync failed"):
                        broker.handle((123, 1000, 1000), request)
                self.assertEqual(broker.store.value, before)
                self.assertEqual(broker.store.path.read_bytes(), durable)

            assert_rollback({"action": "register_anchor", "runtime_generation": 2})
            assert_rollback({
                "action": "mint_token", "session_id": session_id,
                "runtime_generation": 1, "binding": self.binding(),
            })
            assert_rollback({
                "action": "consume_token", "session_id": session_id,
                "runtime_generation": 1, "token": token,
            })

            with tempfile.TemporaryDirectory() as second_directory:
                second = self.make_broker(Path(second_directory))
                with patch.object(second.store, "commit", side_effect=OSError("fsync failed")):
                    with self.assertRaisesRegex(OSError, "fsync failed"):
                        self.register(second)
                self.assertEqual(
                    second.store.value, {"version": 1, "sessions": {}, "tokens": {}}
                )
                self.assertFalse(second.store.path.exists())

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