"""
Tests for pulse.core.pulse_crypto — critical path coverage.
Targets: PulseCrypto.__init__, sign, verify, seal,
         get_agent_key, sign_string, verify_string
"""
from __future__ import annotations

import json
import sys
import time
from pathlib import Path
from unittest.mock import patch
import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from pulse.core.pulse_crypto import PulseCrypto


TEST_SECRET = "test-secret-key-for-unit-tests"


def make_crypto(**kwargs) -> PulseCrypto:
    return PulseCrypto(secret_key=TEST_SECRET, **kwargs)


# ---------------------------------------------------------------------------
# __init__
# ---------------------------------------------------------------------------

class TestPulseCryptoInit:
    def test_init_with_explicit_key(self):
        crypto = PulseCrypto(secret_key=TEST_SECRET)
        assert crypto.secret == TEST_SECRET

    def test_init_loads_from_env(self, monkeypatch):
        monkeypatch.setenv("PULSE_BRAIN_KEY", "env-secret")
        crypto = PulseCrypto()
        assert crypto.secret == "env-secret"

    def test_raises_without_key(self, monkeypatch):
        monkeypatch.delenv("PULSE_BRAIN_KEY", raising=False)
        with pytest.raises(RuntimeError, match="PULSE_BRAIN_KEY"):
            PulseCrypto()

    def test_wipe_env_removes_key(self, monkeypatch):
        monkeypatch.setenv("PULSE_BRAIN_KEY", "wipe-me")
        import os
        PulseCrypto(wipe_env=True)
        assert "PULSE_BRAIN_KEY" not in os.environ

    def test_no_wipe_keeps_key(self, monkeypatch):
        monkeypatch.setenv("PULSE_BRAIN_KEY", "keep-me")
        import os
        PulseCrypto(wipe_env=False)
        assert os.environ.get("PULSE_BRAIN_KEY") == "keep-me"

    def test_key_version_defaults_to_1(self, monkeypatch):
        monkeypatch.delenv("PULSE_KEY_VERSION", raising=False)
        crypto = make_crypto()
        assert crypto.key_version == 1

    def test_key_version_from_env(self, monkeypatch):
        monkeypatch.setenv("PULSE_KEY_VERSION", "3")
        crypto = make_crypto()
        assert crypto.key_version == 3


# ---------------------------------------------------------------------------
# sign
# ---------------------------------------------------------------------------

class TestSign:
    def setup_method(self):
        self.crypto = make_crypto()

    def test_returns_hex_string(self):
        sig = self.crypto.sign({"text": "hello"})
        assert isinstance(sig, str)
        assert len(sig) == 64  # SHA-256 hex = 64 chars

    def test_deterministic(self):
        item = {"a": 1, "b": "two"}
        assert self.crypto.sign(item) == self.crypto.sign(item)

    def test_different_items_different_signatures(self):
        assert self.crypto.sign({"a": 1}) != self.crypto.sign({"a": 2})

    def test_strips_internal_fields(self):
        item = {"text": "data", "_signature": "old", "_hash": "old2", "_key_version": 99, "_signed_at": 0}
        # Signature should be same as clean item
        clean_sig = self.crypto.sign({"text": "data"})
        dirty_sig = self.crypto.sign(item)
        assert clean_sig == dirty_sig

    def test_key_order_invariant(self):
        sig1 = self.crypto.sign({"a": 1, "b": 2})
        sig2 = self.crypto.sign({"b": 2, "a": 1})
        assert sig1 == sig2


# ---------------------------------------------------------------------------
# seal
# ---------------------------------------------------------------------------

class TestSeal:
    def setup_method(self):
        self.crypto = make_crypto()

    def test_seal_adds_required_fields(self):
        item = {"text": "knowledge"}
        sealed = self.crypto.seal(item, "agent-1")
        assert "_signature" in sealed
        assert "_agent_id" in sealed
        assert "_key_version" in sealed
        assert "_signed_at" in sealed
        assert "_hash" in sealed

    def test_seal_sets_agent_id(self):
        item = {"text": "knowledge"}
        sealed = self.crypto.seal(item, "agent-007")
        assert sealed["_agent_id"] == "agent-007"

    def test_seal_sets_key_version(self):
        item = {"x": 1}
        sealed = self.crypto.seal(item, "agent")
        assert sealed["_key_version"] == self.crypto.key_version

    def test_sealed_item_verifies(self):
        item = {"content": "test"}
        sealed = self.crypto.seal(item, "agent")
        assert self.crypto.verify(sealed)

    def test_tampered_item_fails_verify(self):
        item = {"content": "original"}
        sealed = self.crypto.seal(item.copy(), "agent")
        sealed["content"] = "tampered"
        assert not self.crypto.verify(sealed)


# ---------------------------------------------------------------------------
# verify
# ---------------------------------------------------------------------------

class TestVerify:
    def setup_method(self):
        self.crypto = make_crypto()

    def test_valid_signature_returns_true(self):
        item = {"data": "legit"}
        sealed = self.crypto.seal(item, "agent")
        assert self.crypto.verify(sealed)

    def test_missing_signature_returns_false(self):
        item = {"data": "unsigned"}
        assert not self.crypto.verify(item)

    def test_wrong_key_version_returns_false(self, monkeypatch):
        item = {"data": "v1"}
        sealed = self.crypto.seal(item.copy(), "agent")
        sealed["_key_version"] = 99  # wrong version
        assert not self.crypto.verify(sealed)

    def test_stale_signature_rejected(self):
        item = {"data": "old"}
        sealed = self.crypto.seal(item.copy(), "agent")
        # Make it look ancient (signed 10 minutes ago)
        sealed["_signed_at"] = time.time() - 601
        # Re-sign with old timestamp (sign() strips _signed_at from payload)
        sealed["_signature"] = self.crypto.sign(sealed)
        assert not self.crypto.verify(sealed)

    def test_invalid_signed_at_rejected(self):
        item = {"data": "bad_ts"}
        sealed = self.crypto.seal(item.copy(), "agent")
        sealed["_signed_at"] = "not-a-number"
        assert not self.crypto.verify(sealed)

    def test_no_signed_at_still_verifies(self):
        # Items without _signed_at skip replay check
        item = {"data": "legacy"}
        sealed = self.crypto.seal(item.copy(), "agent")
        del sealed["_signed_at"]
        sealed["_signature"] = self.crypto.sign(sealed)
        assert self.crypto.verify(sealed)

    def test_empty_signature_returns_false(self):
        item = {"data": "test", "_signature": ""}
        assert not self.crypto.verify(item)


# ---------------------------------------------------------------------------
# get_agent_key / sign_string / verify_string
# ---------------------------------------------------------------------------

class TestAgentKeySigning:
    def setup_method(self):
        self.crypto = make_crypto()

    def test_get_agent_key_returns_bytes(self):
        key = self.crypto.get_agent_key("agent-1")
        assert isinstance(key, bytes)
        assert len(key) == 32  # SHA-256 = 32 bytes

    def test_different_agents_different_keys(self):
        k1 = self.crypto.get_agent_key("agent-1")
        k2 = self.crypto.get_agent_key("agent-2")
        assert k1 != k2

    def test_same_agent_same_key(self):
        assert self.crypto.get_agent_key("x") == self.crypto.get_agent_key("x")

    def test_sign_string_returns_hex(self):
        sig = self.crypto.sign_string("hello world", "agent-1")
        assert isinstance(sig, str)
        assert len(sig) == 64

    def test_verify_string_valid(self):
        text = "some log line"
        sig = self.crypto.sign_string(text, "agent-1")
        assert self.crypto.verify_string(text, sig, "agent-1")

    def test_verify_string_wrong_agent(self):
        text = "log line"
        sig = self.crypto.sign_string(text, "agent-1")
        assert not self.crypto.verify_string(text, sig, "agent-2")

    def test_verify_string_tampered_text(self):
        text = "original"
        sig = self.crypto.sign_string(text, "agent-1")
        assert not self.crypto.verify_string("tampered", sig, "agent-1")

    def test_verify_string_wrong_signature(self):
        assert not self.crypto.verify_string("hello", "a" * 64, "agent-1")

    def test_sign_string_deterministic(self):
        sig1 = self.crypto.sign_string("abc", "agent")
        sig2 = self.crypto.sign_string("abc", "agent")
        assert sig1 == sig2


# ---------------------------------------------------------------------------
# Two instances with different keys
# ---------------------------------------------------------------------------

class TestCrossKeyIsolation:
    def test_signature_from_key1_fails_key2(self):
        c1 = PulseCrypto(secret_key="secret-A")
        c2 = PulseCrypto(secret_key="secret-B")
        item = {"data": "test"}
        sealed = c1.seal(item.copy(), "agent")
        assert not c2.verify(sealed)

    def test_string_sig_from_key1_fails_key2(self):
        c1 = PulseCrypto(secret_key="secret-A")
        c2 = PulseCrypto(secret_key="secret-B")
        sig = c1.sign_string("hello", "agent")
        assert not c2.verify_string("hello", sig, "agent")
