# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License - see LICENSE file for details

"""
Tests for genesis block, chain store, and bridge layer.
"""

import json
import sqlite3
import sys
from pathlib import Path

import pytest

# Ensure the repo root is on sys.path so `familiar.*` imports resolve
_REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from cryptography.hazmat.primitives.serialization import (  # noqa: E402
    load_pem_private_key,
    load_pem_public_key,
)

from familiar.core.genesis import (  # noqa: E402
    ChainBlock,
    ChainStore,
    GenesisBlock,
    ProfessionalCredentialAnchor,
    ProfessionalOwnershipFramework,
    ProfessionalTemplateDerivation,
    ProvenanceChainInit,
    VerifiedCapabilityAttestation,
    generate_keypair,
    load_or_create_keypair,
)

# ── Fixtures ──


@pytest.fixture
def keypair():
    """Generate a fresh RSA-2048 keypair for testing."""
    priv_pem, pub_pem = generate_keypair()
    priv = load_pem_private_key(priv_pem, password=None)
    pub = load_pem_public_key(pub_pem)
    return priv, pub


@pytest.fixture
def sample_genesis(keypair):
    """Build and sign a sample genesis block."""
    priv, pub = keypair
    gb = GenesisBlock(
        pca=ProfessionalCredentialAnchor(
            credential_hash="abc123",
            orcid="0009-0006-4957-0540",
        ),
        ptd=ProfessionalTemplateDerivation(template_source_id="familiar_v1.4.0"),
        vca=VerifiedCapabilityAttestation(
            expertise_domains={"ai": ["curation"]},
            quality_thresholds={"min": 0.5},
        ),
        pof=ProfessionalOwnershipFramework(
            multi_sig_control={"owner": "test_key"},
        ),
        pci=ProvenanceChainInit(
            creation_timestamp="2026-02-24T00:00:00+00:00",
        ),
    )
    gb.compute_hash()
    gb.sign(priv)
    return gb


@pytest.fixture
def tmp_chain(tmp_path):
    """Return a ChainStore backed by a temp directory."""
    return ChainStore(tmp_path / "chain.db")


@pytest.fixture
def tmp_chain_dir(tmp_path):
    """Return a temp directory for chain data (keys + db)."""
    return tmp_path / "chain"


# ── GenesisBlock tests ──


class TestGenesisBlock:
    def test_compute_hash_deterministic(self):
        gb = GenesisBlock(
            pca=ProfessionalCredentialAnchor(credential_hash="x"),
            ptd=ProfessionalTemplateDerivation(),
            vca=VerifiedCapabilityAttestation(),
            pof=ProfessionalOwnershipFramework(),
            pci=ProvenanceChainInit(),
        )
        h1 = gb.compute_hash()
        h2 = gb.compute_hash()
        assert h1 == h2
        assert len(h1) == 64  # SHA-256 hex

    def test_sign_and_verify(self, keypair, sample_genesis):
        _, pub = keypair
        assert sample_genesis.block_hash
        assert sample_genesis.signature
        assert sample_genesis.verify(pub)

    def test_verify_fails_wrong_key(self, sample_genesis):
        # Generate a different keypair
        other_priv_pem, other_pub_pem = generate_keypair()
        other_pub = load_pem_public_key(other_pub_pem)
        assert not sample_genesis.verify(other_pub)

    def test_verify_fails_tampered_hash(self, keypair, sample_genesis):
        _, pub = keypair
        sample_genesis.block_hash = "tampered" + sample_genesis.block_hash[8:]
        assert not sample_genesis.verify(pub)

    def test_round_trip_serialization(self, sample_genesis):
        d = sample_genesis.to_dict()
        json_str = json.dumps(d)
        restored = GenesisBlock.from_dict(json.loads(json_str))
        assert restored.block_hash == sample_genesis.block_hash
        assert restored.signature == sample_genesis.signature
        assert restored.pca.orcid == "0009-0006-4957-0540"
        assert restored.version == 1


# ── ChainBlock tests ──


class TestChainBlock:
    def test_compute_hash(self):
        block = ChainBlock(
            index=1,
            timestamp="2026-02-24T00:00:00+00:00",
            prev_hash="a" * 64,
            block_hash="",
            block_type="provenance",
            payload={"action": "curate"},
        )
        h = block.compute_hash()
        assert len(h) == 64

    def test_rsa_sign_and_verify(self, keypair):
        priv, pub = keypair
        block = ChainBlock(
            index=1,
            timestamp="2026-02-24T00:00:00+00:00",
            prev_hash="a" * 64,
            block_hash="",
            block_type="provenance",
            payload={"action": "test"},
        )
        block.compute_hash()
        block.sign(priv, algorithm="rsa")
        assert block.verify(pub, algorithm="rsa")

    def test_ed25519_sign_and_verify(self):
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

        priv = Ed25519PrivateKey.generate()
        pub = priv.public_key()
        block = ChainBlock(
            index=1,
            timestamp="2026-02-24T00:00:00+00:00",
            prev_hash="a" * 64,
            block_hash="",
            block_type="training",
            payload={"data": "test"},
        )
        block.compute_hash()
        block.sign(priv, algorithm="ed25519")
        assert block.verify(pub, algorithm="ed25519")


# ── ChainStore tests ──


class TestChainStore:
    def test_append_and_retrieve(self, tmp_chain, keypair):
        priv, _ = keypair
        block = ChainBlock(
            index=0,
            timestamp="2026-02-24T00:00:00+00:00",
            prev_hash="0" * 64,
            block_hash="",
            block_type="genesis",
            payload={"test": True},
        )
        block.compute_hash()
        block.sign(priv)
        tmp_chain.append(block)

        retrieved = tmp_chain.get_genesis()
        assert retrieved is not None
        assert retrieved.block_hash == block.block_hash
        assert retrieved.payload == {"test": True}

    def test_chain_length(self, tmp_chain, keypair):
        priv, _ = keypair
        assert tmp_chain.chain_length() == 0

        prev_hash = "0" * 64
        for i in range(3):
            block = ChainBlock(
                index=i,
                timestamp=f"2026-02-24T0{i}:00:00+00:00",
                prev_hash=prev_hash,
                block_hash="",
                block_type="genesis" if i == 0 else "provenance",
                payload={"i": i},
            )
            block.compute_hash()
            block.sign(priv)
            tmp_chain.append(block)
            prev_hash = block.block_hash

        assert tmp_chain.chain_length() == 3

    def test_get_latest(self, tmp_chain, keypair):
        priv, _ = keypair
        prev_hash = "0" * 64
        for i in range(2):
            block = ChainBlock(
                index=i,
                timestamp=f"2026-02-24T0{i}:00:00+00:00",
                prev_hash=prev_hash,
                block_hash="",
                block_type="genesis" if i == 0 else "provenance",
                payload={"i": i},
            )
            block.compute_hash()
            block.sign(priv)
            tmp_chain.append(block)
            prev_hash = block.block_hash

        latest = tmp_chain.get_latest()
        assert latest.index == 1

    def test_index_continuity_enforced(self, tmp_chain, keypair):
        priv, _ = keypair
        block = ChainBlock(
            index=5,  # Wrong — should be 0
            timestamp="2026-02-24T00:00:00+00:00",
            prev_hash="0" * 64,
            block_hash="",
            block_type="genesis",
            payload={},
        )
        block.compute_hash()
        block.sign(priv)
        with pytest.raises(ValueError, match="Block index 5 != expected 0"):
            tmp_chain.append(block)

    def test_verify_chain_valid(self, tmp_chain, keypair):
        priv, _ = keypair
        prev_hash = "0" * 64
        for i in range(3):
            block = ChainBlock(
                index=i,
                timestamp=f"2026-02-24T0{i}:00:00+00:00",
                prev_hash=prev_hash,
                block_hash="",
                block_type="genesis" if i == 0 else "provenance",
                payload={"i": i},
            )
            block.compute_hash()
            block.sign(priv)
            tmp_chain.append(block)
            prev_hash = block.block_hash

        valid, error = tmp_chain.verify_chain()
        assert valid is True
        assert error is None

    def test_verify_chain_detects_tampered_payload(self, tmp_chain, keypair):
        """Tamper with a block's payload in the DB and verify detection."""
        priv, _ = keypair
        prev_hash = "0" * 64
        for i in range(2):
            block = ChainBlock(
                index=i,
                timestamp=f"2026-02-24T0{i}:00:00+00:00",
                prev_hash=prev_hash,
                block_hash="",
                block_type="genesis" if i == 0 else "provenance",
                payload={"i": i},
            )
            block.compute_hash()
            block.sign(priv)
            tmp_chain.append(block)
            prev_hash = block.block_hash

        # Tamper: modify payload of block 1 directly in SQLite
        with sqlite3.connect(str(tmp_chain.db_path)) as conn:
            conn.execute(
                "UPDATE chain SET payload = ? WHERE idx = 1",
                (json.dumps({"i": 999}),),
            )

        valid, error = tmp_chain.verify_chain()
        assert valid is False
        assert "hash mismatch" in error

    def test_verify_empty_chain(self, tmp_chain):
        valid, error = tmp_chain.verify_chain()
        assert valid is True

    def test_get_chain_slice(self, tmp_chain, keypair):
        priv, _ = keypair
        prev_hash = "0" * 64
        for i in range(5):
            block = ChainBlock(
                index=i,
                timestamp=f"2026-02-24T0{i}:00:00+00:00",
                prev_hash=prev_hash,
                block_hash="",
                block_type="genesis" if i == 0 else "provenance",
                payload={"i": i},
            )
            block.compute_hash()
            block.sign(priv)
            tmp_chain.append(block)
            prev_hash = block.block_hash

        chain = tmp_chain.get_chain(start=2, limit=2)
        assert len(chain) == 2
        assert chain[0].index == 2
        assert chain[1].index == 3


# ── RSA key management tests ──


class TestKeyManagement:
    def test_generate_keypair(self):
        priv_pem, pub_pem = generate_keypair()
        assert b"BEGIN PRIVATE KEY" in priv_pem
        assert b"BEGIN PUBLIC KEY" in pub_pem

    def test_load_or_create_keypair_creates(self, tmp_path):
        key_dir = tmp_path / "keys"
        priv, pub = load_or_create_keypair(key_dir)
        assert (key_dir / "chain_private.pem").exists()
        assert (key_dir / "chain_public.pem").exists()

    def test_load_or_create_keypair_reloads(self, tmp_path):
        key_dir = tmp_path / "keys"
        priv1, pub1 = load_or_create_keypair(key_dir)
        priv2, pub2 = load_or_create_keypair(key_dir)
        # Public keys should be identical on reload
        from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

        assert pub1.public_bytes(
            Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
        ) == pub2.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)


# ── Bridge function tests ──


class TestBridge:
    @pytest.fixture(autouse=True)
    def _setup_chain_dir(self, tmp_path, monkeypatch):
        """Point CHAIN_DIR to a temp directory for all bridge tests."""
        self.chain_dir = tmp_path / "chain"
        self.chain_dir.mkdir()
        monkeypatch.setattr(
            "familiar.skills.training.bridge._get_chain_dir",
            lambda: self.chain_dir,
        )

    def test_create_genesis(self):
        from familiar.skills.training.bridge import create_familiar_genesis, get_chain_status

        block_hash = create_familiar_genesis(
            owner_name="George Scott Foley",
            owner_orcid="0009-0006-4957-0540",
            owner_email="test@example.com",
        )
        assert len(block_hash) == 64
        status = get_chain_status()
        assert status["initialized"] is True
        assert status["chain_length"] == 1

    def test_duplicate_genesis_rejected(self):
        from familiar.skills.training.bridge import create_familiar_genesis

        create_familiar_genesis(
            owner_name="Test",
            owner_orcid="0000-0000-0000-0000",
        )
        with pytest.raises(RuntimeError, match="already exists"):
            create_familiar_genesis(
                owner_name="Test",
                owner_orcid="0000-0000-0000-0000",
            )

    def test_anchor_provenance_no_genesis(self):
        from familiar.skills.training.bridge import anchor_provenance

        result = anchor_provenance({"action": "test"})
        assert result == ""

    def test_anchor_provenance_with_genesis(self):
        from familiar.skills.training.bridge import (
            anchor_provenance,
            create_familiar_genesis,
            get_chain_status,
        )

        create_familiar_genesis(owner_name="Test", owner_orcid="0000")
        block_hash = anchor_provenance(
            {
                "action": "curate_conversations",
                "records_processed": 10,
                "records_accepted": 5,
            }
        )
        assert len(block_hash) == 64
        status = get_chain_status()
        assert status["chain_length"] == 2

    def test_anchor_export_with_genesis(self):
        from familiar.skills.training.bridge import (
            anchor_export,
            create_familiar_genesis,
            get_chain_status,
        )

        create_familiar_genesis(owner_name="Test", owner_orcid="0000")
        block_hash = anchor_export(
            {
                "version": 1,
                "format": "alpaca",
                "export_fingerprint": "a" * 64,
                "total_examples": 42,
            }
        )
        assert len(block_hash) == 64
        status = get_chain_status()
        assert status["chain_length"] == 2
        assert status["latest_block"]["type"] == "export"

    def test_chain_integrity_after_operations(self):
        from familiar.core.genesis import ChainStore
        from familiar.skills.training.bridge import (
            anchor_export,
            anchor_provenance,
            create_familiar_genesis,
        )

        create_familiar_genesis(owner_name="Test", owner_orcid="0000")
        anchor_provenance({"action": "curate"})
        anchor_export({"fingerprint": "abc"})

        store = ChainStore(self.chain_dir / "chain.db")
        valid, error = store.verify_chain()
        assert valid is True
        assert error is None

    def test_get_chain_status_empty(self):
        from familiar.skills.training.bridge import get_chain_status

        status = get_chain_status()
        assert status["initialized"] is False
        assert status["chain_length"] == 0
