import base64
import json
import pytest

from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization

from services.mesh import mesh_hashchain, mesh_crypto, mesh_protocol, mesh_schema


def test_infonet_ingest_accepts_valid_event(tmp_path, monkeypatch):
    monkeypatch.setattr(mesh_hashchain, "DATA_DIR", tmp_path)
    monkeypatch.setattr(mesh_hashchain, "CHAIN_FILE", tmp_path / "infonet.json")

    inf = mesh_hashchain.Infonet()

    priv = ed25519.Ed25519PrivateKey.generate()
    pub = priv.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    pub_b64 = base64.b64encode(pub).decode("utf-8")
    node_id = mesh_crypto.derive_node_id(pub_b64)

    payload = mesh_protocol.normalize_payload(
        "message",
        {"message": "hello", "destination": "broadcast", "channel": "LongFast", "priority": "normal", "ephemeral": False},
    )
    sig_payload = mesh_crypto.build_signature_payload(
        event_type="message", node_id=node_id, sequence=1, payload=payload
    )
    signature = priv.sign(sig_payload.encode("utf-8")).hex()

    evt = mesh_hashchain.ChainEvent(
        prev_hash=mesh_hashchain.GENESIS_HASH,
        event_type="message",
        node_id=node_id,
        payload=payload,
        sequence=1,
        signature=signature,
        public_key=pub_b64,
        public_key_algo="Ed25519",
        protocol_version=mesh_protocol.PROTOCOL_VERSION,
        network_id=mesh_protocol.NETWORK_ID,
    )
    result = inf.ingest_events([evt.to_dict()])

    assert result["accepted"] == 1
    assert inf.head_hash == evt.event_id
    info = inf.get_info()
    assert info["known_nodes"] == 1
    assert info["author_nodes"] == 1
    assert info["total_events"] == 1
    assert info["event_types"]["message"] == 1


def test_verify_node_binding_accepts_current_and_compat_ids_only(monkeypatch):
    priv = ed25519.Ed25519PrivateKey.generate()
    pub = priv.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    pub_b64 = base64.b64encode(pub).decode("utf-8")

    current = mesh_crypto.derive_node_id(pub_b64)
    compat = mesh_crypto.derive_node_id(pub_b64, legacy=True)
    legacy = (
        f"{mesh_crypto.NODE_ID_PREFIX}"
        f"{current[len(mesh_crypto.NODE_ID_PREFIX):len(mesh_crypto.NODE_ID_PREFIX) + 8]}"
    )

    monkeypatch.setenv("MESH_DEV_ALLOW_LEGACY_COMPAT", "true")
    monkeypatch.setenv("MESH_BLOCK_LEGACY_NODE_ID_COMPAT", "false")
    monkeypatch.setenv("MESH_ALLOW_LEGACY_NODE_ID_COMPAT_UNTIL", "2099-01-01")
    from services.config import get_settings

    get_settings.cache_clear()
    try:
        assert mesh_crypto.verify_node_binding(current, pub_b64)
        assert mesh_crypto.verify_node_binding(compat, pub_b64)
        assert not mesh_crypto.verify_node_binding(legacy, pub_b64)
    finally:
        get_settings.cache_clear()


def test_infonet_append_rejects_missing_signature_fields(tmp_path, monkeypatch):
    monkeypatch.setattr(mesh_hashchain, "DATA_DIR", tmp_path)
    monkeypatch.setattr(mesh_hashchain, "CHAIN_FILE", tmp_path / "infonet.json")

    inf = mesh_hashchain.Infonet()
    payload = mesh_protocol.normalize_payload(
        "message",
        {"message": "hello", "destination": "broadcast", "channel": "LongFast", "priority": "normal", "ephemeral": False},
    )

    try:
        inf.append(
            event_type="message",
            node_id="!sb_test",
            payload=payload,
            sequence=1,
        )
        assert False, "Expected missing signature fields to be rejected"
    except ValueError as exc:
        assert "signature" in str(exc).lower()


def test_infonet_load_quarantines_and_resets_on_hash_mismatch(tmp_path, monkeypatch):
    monkeypatch.setattr(mesh_hashchain, "DATA_DIR", tmp_path)
    monkeypatch.setattr(mesh_hashchain, "CHAIN_FILE", tmp_path / "infonet.json")

    priv = ed25519.Ed25519PrivateKey.generate()
    pub = priv.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    pub_b64 = base64.b64encode(pub).decode("utf-8")
    node_id = mesh_crypto.derive_node_id(pub_b64)
    payload = mesh_protocol.normalize_payload(
        "message",
        {"message": "hello", "destination": "broadcast", "channel": "LongFast", "priority": "normal", "ephemeral": False},
    )
    sig_payload = mesh_crypto.build_signature_payload(
        event_type="message", node_id=node_id, sequence=1, payload=payload
    )
    signature = priv.sign(sig_payload.encode("utf-8")).hex()
    evt = mesh_hashchain.ChainEvent(
        prev_hash=mesh_hashchain.GENESIS_HASH,
        event_type="message",
        node_id=node_id,
        payload=payload,
        sequence=1,
        signature=signature,
        public_key=pub_b64,
        public_key_algo="Ed25519",
        protocol_version=mesh_protocol.PROTOCOL_VERSION,
        network_id=mesh_protocol.NETWORK_ID,
    ).to_dict()
    evt["event_id"] = "tampered"
    mesh_hashchain.CHAIN_FILE.write_text(
        json.dumps({"events": [evt], "head_hash": "tampered", "node_sequences": {node_id: 1}}),
        encoding="utf-8",
    )

    inf = mesh_hashchain.Infonet()

    assert inf.events == []
    assert inf.head_hash == mesh_hashchain.GENESIS_HASH
    assert not mesh_hashchain.CHAIN_FILE.exists()
    assert list(tmp_path.glob("infonet.json.quarantine.*"))


def test_validate_gate_message_payload_rejects_plaintext_shape():
    payload = mesh_protocol.normalize_payload(
        "gate_message",
        {"gate": "infonet", "message": "plaintext should fail"},
    )

    valid, reason = mesh_schema.validate_event_payload("gate_message", payload)

    assert valid is False
    assert reason == "epoch must be a positive integer"


def test_gate_store_accepts_encrypted_gate_payload(tmp_path, monkeypatch):
    priv = ed25519.Ed25519PrivateKey.generate()
    pub = priv.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    pub_b64 = base64.b64encode(pub).decode("utf-8")
    node_id = mesh_crypto.derive_node_id(pub_b64)

    payload = mesh_protocol.normalize_payload(
        "gate_message",
        {
            "gate": "infonet",
            "epoch": 2,
            "ciphertext": "opaque-ciphertext",
            "nonce": "nonce-2",
            "sender_ref": "persona-ops-1",
            "format": "mls1",
        },
    )
    sig_payload = mesh_crypto.build_signature_payload(
        event_type="gate_message", node_id=node_id, sequence=1, payload=payload
    )
    signature = priv.sign(sig_payload.encode("utf-8")).hex()

    store = mesh_hashchain.GateMessageStore(data_dir=str(tmp_path / "gate_messages"))
    evt = store.append(
        "infonet",
        {
            "event_id": "evt_gate_cipher",
            "event_type": "gate_message",
            "node_id": node_id,
            "payload": payload,
            "timestamp": 1_700_000_000.0,
            "sequence": 1,
            "signature": signature,
            "public_key": pub_b64,
            "public_key_algo": "Ed25519",
            "protocol_version": mesh_protocol.PROTOCOL_VERSION,
        },
    )

    assert evt["payload"]["gate"] == "infonet"
    assert evt["payload"]["ciphertext"] == "opaque-ciphertext"
    assert evt["payload"]["epoch"] == 2
    assert evt["payload"]["nonce"] == "nonce-2"
    assert evt["payload"]["sender_ref"] == "persona-ops-1"


def test_gate_store_rejects_replayed_ciphertext_across_append_and_peer_ingest(tmp_path):
    store = mesh_hashchain.GateMessageStore(data_dir=str(tmp_path / "gate_messages"))
    gate_id = "infonet"
    replay_ts = float(int(mesh_hashchain.time.time() / 60) * 60)
    replay_nonce = "stable-nonce"
    first = store.append(
        gate_id,
        {
            "event_id": "a" * 64,
            "event_type": "gate_message",
            "payload": {
                "gate": gate_id,
                "ciphertext": "opaque-ciphertext",
                "nonce": replay_nonce,
                "format": "mls1",
            },
            "timestamp": replay_ts,
        },
    )

    replayed = store.append(
        gate_id,
        {
            "event_id": "b" * 64,
            "event_type": "gate_message",
            "payload": {
                "gate": gate_id,
                "ciphertext": "opaque-ciphertext",
                "nonce": replay_nonce,
                "format": "mls1",
            },
            "timestamp": replay_ts,
        },
    )
    peer_result = store.ingest_peer_events(
        gate_id,
        [
            {
                "event_type": "gate_message",
                "timestamp": replay_ts,
                "payload": {
                    "gate": gate_id,
                    "ciphertext": "opaque-ciphertext",
                    "nonce": replay_nonce,
                    "format": "mls1",
                },
            }
        ],
    )

    assert replayed is first
    assert peer_result == {"accepted": 0, "duplicates": 1, "rejected": 0}
    assert len(store.get_messages(gate_id, limit=10)) == 1


def test_gate_store_prunes_stale_replay_fingerprints(tmp_path):
    store = mesh_hashchain.GateMessageStore(data_dir=str(tmp_path / "gate_messages"))
    old_ts = mesh_hashchain.time.time() - mesh_hashchain.GATE_REPLAY_WINDOW_S - 10

    store.append(
        "infonet",
        {
            "event_id": "c" * 64,
            "event_type": "gate_message",
            "payload": {
                "gate": "infonet",
                "ciphertext": "old-cipher",
                "format": "mls1",
            },
            "timestamp": old_ts,
        },
    )

    assert len(store._replay_index) == 1
    removed = store._prune_replay_index(now=mesh_hashchain.time.time())

    assert removed == 1
    assert store._replay_index == {}


def test_gate_replay_fingerprint_includes_nonce():
    base = {
        "event_type": "gate_message",
        "timestamp": 1_700_000_000,
        "payload": {
            "gate": "infonet",
            "ciphertext": "same-ciphertext",
            "format": "mls1",
        },
    }
    first = mesh_hashchain.build_gate_replay_fingerprint(
        "infonet",
        {
            **base,
            "payload": {**base["payload"], "nonce": "nonce-a"},
        },
    )
    second = mesh_hashchain.build_gate_replay_fingerprint(
        "infonet",
        {
            **base,
            "payload": {**base["payload"], "nonce": "nonce-b"},
        },
    )

    assert first != second


def test_gate_replay_fingerprint_includes_timestamp():
    base = {
        "event_type": "gate_message",
        "payload": {
            "gate": "infonet",
            "ciphertext": "same-ciphertext",
            "nonce": "nonce-a",
            "format": "mls1",
        },
    }
    first = mesh_hashchain.build_gate_replay_fingerprint(
        "infonet",
        {
            **base,
            "timestamp": 1_700_000_000,
        },
    )
    second = mesh_hashchain.build_gate_replay_fingerprint(
        "infonet",
        {
            **base,
            "timestamp": 1_700_000_001,
        },
    )

    assert first != second
