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

"""
Tests for the Chain-of-Provenance Training Pipeline.

Covers:
  Phase 1: Pipeline orchestrator, CLI, paths
  Phase 2: Consent & data governance
  Phase 3: Co-op data pooling
  Phase 4: Model training integration
  Phase 5: Merkle tree, chain anchor, Dross ledger, model packaging
"""

import json
from pathlib import Path
from unittest.mock import patch

import pytest

# ── Fixtures ──────────────────────────────────────────────────────────────


@pytest.fixture
def tmp_data_dir(tmp_path):
    """Create a temporary data directory and redirect paths."""
    data_dir = tmp_path / "data"
    data_dir.mkdir()
    (data_dir / "training").mkdir()
    (data_dir / "training" / "curated").mkdir(parents=True)
    (data_dir / "training" / "exports").mkdir(parents=True)
    (data_dir / "coop").mkdir()
    (data_dir / "dross").mkdir()
    (data_dir / "chain").mkdir()
    return data_dir


@pytest.fixture
def sample_records():
    """Sample curated training records."""
    return [
        {
            "id": "abc123",
            "source_type": "conversation",
            "instruction": "How do I create a task?",
            "response": "You can create a task by saying 'add task' followed by the description.",
            "quality_score": 0.7,
            "quality_signals": {"response_length": 70},
            "provenance": {
                "author": {"id": "test_user", "name": "Test User"},
                "license": "proprietary",
                "consent": "explicit_opt_in",
                "curated_at": "2026-03-06T10:00:00",
                "curator_version": "1.1.0",
            },
        },
        {
            "id": "def456",
            "source_type": "tool_call",
            "instruction": "Schedule a meeting for tomorrow",
            "response": "Meeting scheduled for tomorrow at 9 AM.",
            "quality_score": 0.8,
            "quality_signals": {"response_length": 40},
            "provenance": {
                "author": {"id": "test_user", "name": "Test User"},
                "license": "proprietary",
                "consent": "explicit_opt_in",
                "curated_at": "2026-03-06T11:00:00",
                "curator_version": "1.1.0",
            },
        },
    ]


def _write_jsonl(path, records):
    """Helper to write records as JSONL."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r) + "\n")


# ══════════════════════════════════════════════════════════════
# Phase 1: Pipeline Orchestrator
# ══════════════════════════════════════════════════════════════


class TestPipelineConfig:
    def test_default_config(self):
        from familiar.skills.training.pipeline import PipelineConfig

        config = PipelineConfig()
        assert config.min_records == 10
        assert config.min_avg_quality == 0.5
        assert config.min_diversity == 2
        assert config.export_format == "alpaca"
        assert config.include_memories is True

    def test_custom_config(self):
        from familiar.skills.training.pipeline import PipelineConfig

        config = PipelineConfig(min_records=0, min_avg_quality=0.3, export_format="sharegpt")
        assert config.min_records == 0
        assert config.min_avg_quality == 0.3
        assert config.export_format == "sharegpt"


class TestPipelineRunState:
    def test_run_state_creation(self):
        from familiar.skills.training.pipeline import PipelineRunState

        run = PipelineRunState(
            run_id="test123",
            status="started",
            started_at="2026-03-06T10:00:00",
        )
        assert run.run_id == "test123"
        assert run.status == "started"
        assert run.completed_at == ""
        assert run.error == ""
        assert run.stats == {}


class TestQualityGate:
    def test_quality_gate_pass(self, tmp_data_dir, sample_records):
        from familiar.skills.training.pipeline import PipelineConfig, _quality_gate

        # Write sample records to curated dir
        curated_dir = tmp_data_dir / "training" / "curated"
        _write_jsonl(curated_dir / "conversations.jsonl", [sample_records[0]])
        _write_jsonl(curated_dir / "tool_calls.jsonl", [sample_records[1]])

        with patch("familiar.skills.training.pipeline._get_data_dir", return_value=tmp_data_dir):
            with patch("familiar.skills.training.skill._get_data_dir", return_value=tmp_data_dir):
                config = PipelineConfig(min_records=2, min_diversity=2, min_avg_quality=0.5)
                passed, details = _quality_gate(config)

        assert passed is True
        assert details["total_records"] == 2
        assert details["distinct_sources"] == 2

    def test_quality_gate_fail_records(self, tmp_data_dir, sample_records):
        from familiar.skills.training.pipeline import PipelineConfig, _quality_gate

        curated_dir = tmp_data_dir / "training" / "curated"
        _write_jsonl(curated_dir / "conversations.jsonl", [sample_records[0]])

        with patch("familiar.skills.training.pipeline._get_data_dir", return_value=tmp_data_dir):
            with patch("familiar.skills.training.skill._get_data_dir", return_value=tmp_data_dir):
                config = PipelineConfig(min_records=10, min_diversity=1)
                passed, details = _quality_gate(config)

        assert passed is False
        assert "failure_reasons" in details


class TestPipelineStatus:
    def test_get_pipeline_status_empty(self, tmp_data_dir):
        from familiar.skills.training.pipeline import get_pipeline_status

        with patch("familiar.skills.training.pipeline._get_data_dir", return_value=tmp_data_dir):
            status = get_pipeline_status()

        assert status["total_runs"] == 0
        assert status["latest_run"] is None


class TestTrainCLI:
    def test_create_parser(self):
        from familiar.core.train_cli import create_parser

        parser = create_parser()
        # Test that subcommands exist
        args = parser.parse_args(["status"])
        assert args.command == "status"

    def test_run_args(self):
        from familiar.core.train_cli import create_parser

        parser = create_parser()
        args = parser.parse_args(["run", "--format", "sharegpt", "--min-records", "0"])
        assert args.format == "sharegpt"
        assert args.min_records == 0

    def test_stats_args(self):
        from familiar.core.train_cli import create_parser

        parser = create_parser()
        args = parser.parse_args(["stats", "--detailed"])
        assert args.detailed is True


# ══════════════════════════════════════════════════════════════
# Phase 2: Consent & Data Governance
# ══════════════════════════════════════════════════════════════


class TestConsentScope:
    def test_scope_values(self):
        from familiar.core.consent import ConsentScope

        assert ConsentScope.LOCAL_TRAINING.value == "local_training"
        assert ConsentScope.COOP_SHARING.value == "coop_sharing"
        assert ConsentScope.PUBLIC_DATASET.value == "public_dataset"
        assert ConsentScope.NONE.value == "none"


class TestDataClassification:
    def test_classification_values(self):
        from familiar.core.consent import DataClassification

        assert DataClassification.PRIVATE.value == "private"
        assert DataClassification.SHAREABLE.value == "shareable"
        assert DataClassification.PUBLIC.value == "public"


class TestConsentManager:
    def test_grant_consent(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager

        manager = ConsentManager(data_dir=tmp_data_dir)
        record = manager.grant_consent("user1", ["local_training"])

        assert record.user_id == "user1"
        assert "local_training" in record.scopes
        assert record.granted_at != ""

    def test_check_consent(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("user1", ["local_training", "coop_sharing"])

        assert manager.check_consent("user1", "local_training") is True
        assert manager.check_consent("user1", "coop_sharing") is True
        assert manager.check_consent("user1", "public_dataset") is False

    def test_revoke_consent(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("user1", ["local_training", "coop_sharing"])
        manager.revoke_consent("user1", ["coop_sharing"])

        assert manager.check_consent("user1", "local_training") is True
        assert manager.check_consent("user1", "coop_sharing") is False

    def test_revoke_all_consent(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("user1", ["local_training"])
        record = manager.revoke_consent("user1", ["local_training"])

        assert record.scopes == []
        assert record.revoked_at != ""
        assert manager.check_consent("user1", "local_training") is False

    def test_get_consent_nonexistent(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager

        manager = ConsentManager(data_dir=tmp_data_dir)
        assert manager.get_consent("nobody") is None

    def test_classify_record(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager, DataClassification

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("user1", ["local_training", "coop_sharing"])

        classification = manager.classify_record({}, "user1")
        assert classification == DataClassification.SHAREABLE

    def test_classify_public(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager, DataClassification

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("user1", ["local_training", "coop_sharing", "public_dataset"])

        classification = manager.classify_record({}, "user1")
        assert classification == DataClassification.PUBLIC

    def test_erase_user_data(self, tmp_data_dir, sample_records):
        from familiar.core.consent import ConsentManager

        # Write sample records
        curated_dir = tmp_data_dir / "training" / "curated"
        _write_jsonl(curated_dir / "conversations.jsonl", sample_records)

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("test_user", ["local_training"])

        with patch("familiar.core.consent.ConsentManager._anchor_erasure", return_value=""):
            with patch("familiar.skills.training.skill._get_data_dir", return_value=tmp_data_dir):
                result = manager.erase_user_data("test_user")

        assert result["records_deleted"] == 2
        assert manager.check_consent("test_user", "local_training") is False

    def test_audit_log(self, tmp_data_dir):
        from familiar.core.consent import ConsentManager

        manager = ConsentManager(data_dir=tmp_data_dir)
        manager.grant_consent("user1", ["local_training"])
        manager.revoke_consent("user1", ["local_training"])

        audit = manager.get_audit_log("user1")
        assert len(audit) == 2
        assert audit[0]["action"] == "revoke"
        assert audit[1]["action"] == "grant"


# ══════════════════════════════════════════════════════════════
# Phase 3: Co-op Data Pooling
# ══════════════════════════════════════════════════════════════


class TestCoopRules:
    def test_default_rules(self):
        from familiar.skills.training.coop import CoopRules

        rules = CoopRules()
        assert rules.min_quality == 0.5
        assert "coop_sharing" in rules.required_consent
        assert rules.max_records_per_member == 10000


class TestCoopManager:
    def test_create_coop(self, tmp_data_dir):
        from familiar.skills.training.coop import CoopManager

        manager = CoopManager(data_dir=tmp_data_dir / "coop")
        manifest = manager.create_coop(name="Test Co-op", description="A test co-op")

        assert manifest.name == "Test Co-op"
        assert manifest.coop_id != ""
        assert len(manifest.members) == 1
        assert manifest.created_at != ""

    def test_list_coops(self, tmp_data_dir):
        from familiar.skills.training.coop import CoopManager

        manager = CoopManager(data_dir=tmp_data_dir / "coop")
        manager.create_coop(name="Co-op A")
        manager.create_coop(name="Co-op B")

        coops = manager.list_coops()
        assert len(coops) == 2
        names = {c["name"] for c in coops}
        assert "Co-op A" in names
        assert "Co-op B" in names

    def test_contribute_no_data(self, tmp_data_dir):
        from familiar.skills.training.coop import CoopManager

        coop_dir = tmp_data_dir / "coop"
        manager = CoopManager(data_dir=coop_dir)
        manifest = manager.create_coop(name="Test")

        with patch("familiar.skills.training.skill._get_data_dir", return_value=tmp_data_dir):
            result = manager.contribute(manifest.coop_id)

        assert result.get("records_contributed", 0) == 0

    def test_contribute_with_data(self, tmp_data_dir, sample_records):
        from familiar.skills.training.coop import CoopManager

        # Write sample data
        curated_dir = tmp_data_dir / "training" / "curated"
        _write_jsonl(curated_dir / "conversations.jsonl", sample_records)

        coop_dir = tmp_data_dir / "coop"
        manager = CoopManager(data_dir=coop_dir)
        manifest = manager.create_coop(name="Test")

        with patch("familiar.skills.training.skill._get_data_dir", return_value=tmp_data_dir):
            # Skip consent check for test
            with patch("familiar.core.consent.ConsentManager.check_consent", return_value=True):
                result = manager.contribute(manifest.coop_id)

        assert result.get("records_contributed", 0) == 2

    def test_merge_contributions(self, tmp_data_dir, sample_records):
        from familiar.skills.training.coop import CoopManager

        coop_dir = tmp_data_dir / "coop"
        manager = CoopManager(data_dir=coop_dir)
        manifest = manager.create_coop(name="Test")

        # Write contributions directly
        contrib_dir = coop_dir / manifest.coop_id / "contributions"
        contrib_dir.mkdir(parents=True, exist_ok=True)
        _write_jsonl(contrib_dir / "member1.jsonl", sample_records)

        result = manager.merge_contributions(manifest.coop_id)
        assert result["merged_records"] == 2
        assert result["contributors"] == 1

    def test_export_coop_dataset(self, tmp_data_dir, sample_records):
        from familiar.skills.training.coop import CoopManager

        coop_dir = tmp_data_dir / "coop"
        manager = CoopManager(data_dir=coop_dir)
        manifest = manager.create_coop(name="Test")

        # Write and merge
        contrib_dir = coop_dir / manifest.coop_id / "contributions"
        contrib_dir.mkdir(parents=True, exist_ok=True)
        _write_jsonl(contrib_dir / "member1.jsonl", sample_records)
        manager.merge_contributions(manifest.coop_id)

        path = manager.export_coop_dataset(manifest.coop_id, fmt="alpaca")
        assert Path(path).exists()


class TestCoopTransport:
    def test_export_import_bundle(self, tmp_data_dir, sample_records):
        from familiar.skills.training.coop import CoopManager
        from familiar.skills.training.coop_transport import CoopTransport

        coop_dir = tmp_data_dir / "coop"
        manager = CoopManager(data_dir=coop_dir)
        manifest = manager.create_coop(name="Transport Test")

        # Write contribution
        contrib_dir = coop_dir / manifest.coop_id / "contributions"
        contrib_dir.mkdir(parents=True, exist_ok=True)
        _write_jsonl(contrib_dir / "local.jsonl", sample_records)

        transport = CoopTransport(coop_dir=coop_dir)
        bundle_path = transport.export_bundle(manifest.coop_id)
        assert bundle_path.exists()

        # Import into a separate dir
        import_dir = tmp_data_dir / "coop_import"
        import_dir.mkdir()
        transport2 = CoopTransport(coop_dir=import_dir)
        result = transport2.import_bundle(str(bundle_path))

        assert result["imported"] == 2
        assert result["coop_id"] == manifest.coop_id


# ══════════════════════════════════════════════════════════════
# Phase 4: Model Training Integration
# ══════════════════════════════════════════════════════════════


class TestTrainingConfig:
    def test_default_config(self):
        from familiar.skills.training.model_training import TrainingConfig

        config = TrainingConfig()
        assert config.base_model == "qwen2.5:3b"
        assert config.lora_rank == 8
        assert config.epochs == 3


class TestTrainingRunner:
    def test_detect_backend_stub(self):
        from familiar.skills.training.model_training import TrainingRunner

        runner = TrainingRunner()
        # In test environment, likely stub (no GPU packages)
        assert runner.backend in ("stub", "peft", "unsloth", "ollama")

    def test_install_instructions(self):
        from familiar.skills.training.model_training import TrainingRunner

        runner = TrainingRunner()
        if runner.backend == "stub":
            instructions = runner._get_install_instructions()
            assert "pip install" in instructions


class TestModelCard:
    def test_generate_basic_card(self):
        from familiar.skills.training.model_card import generate_model_card

        card = generate_model_card("test-model")
        assert "# Model Card: test-model" in card
        assert "Ethical Considerations" in card

    def test_generate_card_with_dataset(self):
        from familiar.skills.training.model_card import generate_model_card

        dataset_meta = {
            "total_examples": 100,
            "format": "alpaca",
            "min_quality": 0.5,
            "avg_quality_score": 0.72,
            "export_fingerprint": "abc123" * 8,
            "source_counts": {"conversations": 80, "tool_calls": 20},
            "provenance_summary": {
                "authors": ["user1", "user2"],
                "all_consent_types": ["explicit_opt_in"],
                "license": "proprietary",
            },
        }

        card = generate_model_card("test-model", dataset_meta=dataset_meta)
        assert "100" in card
        assert "conversations" in card
        assert "explicit_opt_in" in card

    def test_generate_card_with_training_run(self):
        from familiar.skills.training.model_card import generate_model_card

        training_run = {
            "backend": "peft",
            "model_hash": "xyz789" * 8,
            "started_at": "2026-03-06T10:00:00",
            "config": {
                "base_model": "qwen2.5:3b",
                "lora_rank": 8,
                "lora_alpha": 16,
                "epochs": 3,
                "learning_rate": 0.0002,
                "batch_size": 4,
                "max_seq_length": 2048,
            },
            "metrics": {"loss": 0.15},
        }

        card = generate_model_card("test-model", training_run=training_run)
        assert "peft" in card
        assert "LoRA rank" in card
        assert "0.15" in card

    def test_generate_card_with_coop(self):
        from familiar.skills.training.model_card import generate_model_card

        coop = {
            "name": "Open AI Co-op",
            "coop_id": "abc123",
            "members": [{"genesis_hash": "h1"}, {"genesis_hash": "h2"}],
            "contribution_stats": {
                "h1": {"records_contributed": 50},
                "h2": {"records_contributed": 30},
            },
        }

        card = generate_model_card("coop-model", coop_manifest=coop)
        assert "Open AI Co-op" in card
        assert "80 records" in card


# ══════════════════════════════════════════════════════════════
# Phase 5: Merkle Tree, Chain Anchor, Dross, Model Package
# ══════════════════════════════════════════════════════════════


class TestMerkleTree:
    def test_single_leaf(self):
        from familiar.core.merkle import MerkleTree

        tree = MerkleTree(["abc123"])
        assert tree.root == "abc123"
        assert tree.leaf_count == 1
        assert tree.depth == 0

    def test_two_leaves(self):
        from familiar.core.merkle import MerkleTree

        tree = MerkleTree(["aaa", "bbb"])
        assert tree.root != "aaa"
        assert tree.root != "bbb"
        assert tree.leaf_count == 2

    def test_proof_verification(self):
        from familiar.core.merkle import MerkleTree

        hashes = ["h1", "h2", "h3", "h4"]
        tree = MerkleTree(hashes)

        for i in range(len(hashes)):
            proof = tree.get_proof(i)
            assert MerkleTree.verify_proof(hashes[i], proof, tree.root)

    def test_proof_invalid(self):
        from familiar.core.merkle import MerkleTree

        tree = MerkleTree(["h1", "h2", "h3", "h4"])
        proof = tree.get_proof(0)
        assert not MerkleTree.verify_proof("wrong_hash", proof, tree.root)

    def test_odd_leaves(self):
        from familiar.core.merkle import MerkleTree

        tree = MerkleTree(["a", "b", "c"])
        assert tree.leaf_count == 3
        # Should still work
        proof = tree.get_proof(2)
        assert MerkleTree.verify_proof("c", proof, tree.root)

    def test_empty_raises(self):
        from familiar.core.merkle import MerkleTree

        with pytest.raises(ValueError):
            MerkleTree([])

    def test_to_dict(self):
        from familiar.core.merkle import MerkleTree

        tree = MerkleTree(["a", "b"])
        d = tree.to_dict()
        assert "root" in d
        assert d["leaf_count"] == 2
        assert "leaves" in d


class TestLocalLedger:
    def test_anchor_and_verify(self, tmp_data_dir):
        from familiar.core.chain_anchor import LocalLedger

        ledger = LocalLedger(db_path=tmp_data_dir / "chain" / "anchors.db")
        tx = ledger.anchor_root("merkle_root_hash", {"block_range": {"start": 0, "end": 10}})

        assert tx != ""
        result = ledger.verify_root(tx)
        assert result["verified"] is True
        assert result["merkle_root"] == "merkle_root_hash"

    def test_verify_nonexistent(self, tmp_data_dir):
        from familiar.core.chain_anchor import LocalLedger

        ledger = LocalLedger(db_path=tmp_data_dir / "chain" / "anchors.db")
        result = ledger.verify_root("nonexistent")
        assert result["verified"] is False

    def test_balance(self, tmp_data_dir):
        from familiar.core.chain_anchor import LocalLedger

        ledger = LocalLedger(db_path=tmp_data_dir / "chain" / "anchors.db")
        balance = ledger.get_balance("addr1")
        assert balance["balance"] == 0.0

        ledger.update_balance("addr1", 100.0)
        balance = ledger.get_balance("addr1")
        assert balance["balance"] == 100.0

    def test_unsynced_anchors(self, tmp_data_dir):
        from familiar.core.chain_anchor import LocalLedger

        ledger = LocalLedger(db_path=tmp_data_dir / "chain" / "anchors.db")
        tx1 = ledger.anchor_root("root1", {})
        ledger.anchor_root("root2", {})

        unsynced = ledger.get_unsynced_anchors()
        assert len(unsynced) == 2

        ledger.mark_synced(tx1, "l2_hash_1")
        unsynced = ledger.get_unsynced_anchors()
        assert len(unsynced) == 1


class TestArbitrumStub:
    def test_raises_not_implemented(self):
        from familiar.core.chain_anchor import ArbitrumAnchor

        anchor = ArbitrumAnchor()
        with pytest.raises(NotImplementedError):
            anchor.anchor_root("root", {})

        with pytest.raises(NotImplementedError):
            anchor.verify_root("tx")

        with pytest.raises(NotImplementedError):
            anchor.get_balance("addr")


class TestSolanaStub:
    def test_raises_not_implemented(self):
        from familiar.core.chain_anchor import SolanaAnchor

        anchor = SolanaAnchor()
        with pytest.raises(NotImplementedError):
            anchor.anchor_root("root", {})


class TestDrossLedger:
    def test_mint(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        entry = ledger.mint(
            "addr1",
            {
                "quality_score": 0.8,
                "record_count": 10,
                "consent_scope": "local_training",
            },
        )

        assert entry.amount == 8.0  # 0.8 * 10 * 1.0
        assert entry.address == "addr1"
        assert entry.reason == "contribution"

    def test_mint_coop_multiplier(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        entry = ledger.mint(
            "addr1",
            {
                "quality_score": 0.8,
                "record_count": 10,
                "consent_scope": "coop_sharing",
            },
        )

        assert entry.amount == 12.0  # 0.8 * 10 * 1.5

    def test_mint_public_multiplier(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        entry = ledger.mint(
            "addr1",
            {
                "quality_score": 0.8,
                "record_count": 10,
                "consent_scope": "public_dataset",
            },
        )

        assert entry.amount == 16.0  # 0.8 * 10 * 2.0

    def test_balance(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        ledger.mint(
            "addr1", {"quality_score": 0.5, "record_count": 10, "consent_scope": "local_training"}
        )
        ledger.mint(
            "addr1", {"quality_score": 0.8, "record_count": 5, "consent_scope": "local_training"}
        )

        balance = ledger.balance("addr1")
        assert balance == 9.0  # (0.5*10*1.0) + (0.8*5*1.0) = 5.0 + 4.0

    def test_quality_bonus(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")

        # Below threshold
        entry = ledger.mint_quality_bonus("addr1", 0.5)
        assert entry.amount == 0.0

        # Above threshold
        entry = ledger.mint_quality_bonus("addr1", 0.9)
        assert entry.amount == 9.0  # 0.9 * 10

    def test_total_supply(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        ledger.mint(
            "addr1", {"quality_score": 0.5, "record_count": 10, "consent_scope": "local_training"}
        )
        ledger.mint(
            "addr2", {"quality_score": 0.8, "record_count": 10, "consent_scope": "local_training"}
        )

        total = ledger.total_supply()
        assert total == 13.0  # 5.0 + 8.0

    def test_top_contributors(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        ledger.mint(
            "addr1", {"quality_score": 0.5, "record_count": 10, "consent_scope": "local_training"}
        )
        ledger.mint(
            "addr2", {"quality_score": 0.8, "record_count": 20, "consent_scope": "local_training"}
        )

        top = ledger.top_contributors(limit=2)
        assert len(top) == 2
        assert top[0]["address"] == "addr2"  # Higher balance

    def test_ledger_entries(self, tmp_data_dir):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        ledger.mint(
            "addr1", {"quality_score": 0.5, "record_count": 10, "consent_scope": "local_training"}
        )

        entries = ledger.ledger("addr1")
        assert len(entries) == 1
        assert entries[0]["address"] == "addr1"

    def test_sync_to_l2(self, tmp_data_dir):
        from familiar.core.chain_anchor import LocalLedger
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_data_dir / "dross" / "dross.db")
        ledger.mint(
            "addr1", {"quality_score": 0.5, "record_count": 10, "consent_scope": "local_training"}
        )

        anchor = LocalLedger(db_path=tmp_data_dir / "chain" / "anchors.db")
        result = ledger.sync_to_l2(anchor)

        assert result["synced"] == 1
        assert result["total_supply"] == 5.0


class TestModelPackage:
    def test_package_and_verify(self, tmp_path):
        from familiar.skills.training.model_package import (
            package_model,
            verify_model_package,
        )

        # Create a fake model directory
        model_dir = tmp_path / "test_model"
        model_dir.mkdir()
        (model_dir / "weights.bin").write_bytes(b"fake model weights")
        (model_dir / "config.json").write_text('{"model": "test"}')

        with patch(
            "familiar.skills.training.model_package._get_genesis_hash", return_value="test_genesis"
        ):
            with patch("familiar.skills.training.model_package._sign_hash", return_value=""):
                pkg = package_model(
                    model_path=str(model_dir),
                    model_card="# Test Model\nA test model card.",
                    genesis_hash="test_genesis",
                )

        assert pkg.exists()
        assert pkg.suffix == ".familiar-model"

        # Verify
        result = verify_model_package(str(pkg))
        assert result["verified"] is True
        assert result["checks"]["zip_integrity"] == "ok"
        assert result["checks"]["provenance"] == "present"
        assert result["checks"]["model_card"] == "present"
        assert result["model_files"] == 2


# ══════════════════════════════════════════════════════════════
# Paths Integration
# ══════════════════════════════════════════════════════════════


class TestPaths:
    def test_coop_dir_exists(self):
        from familiar.core.paths import COOP_DIR

        assert COOP_DIR is not None
        assert str(COOP_DIR).endswith("coop")

    def test_dross_dir_exists(self):
        from familiar.core.paths import DROSS_DIR

        assert DROSS_DIR is not None
        assert str(DROSS_DIR).endswith("dross")

    def test_tenant_scoping(self, tmp_path):
        import familiar.core.paths as paths

        # Save originals
        original_coop = paths.COOP_DIR
        original_dross = paths.DROSS_DIR

        try:
            paths.set_data_root(str(tmp_path))
            assert str(paths.COOP_DIR) == str(tmp_path / "data" / "coop")
            assert str(paths.DROSS_DIR) == str(tmp_path / "data" / "dross")
        finally:
            # Restore
            paths.COOP_DIR = original_coop
            paths.DROSS_DIR = original_dross


# ══════════════════════════════════════════════════════════════
# Dependencies
# ══════════════════════════════════════════════════════════════


class TestDeps:
    def test_training_packages_defined(self):
        from familiar.core.deps import TRAINING_PACKAGES

        assert len(TRAINING_PACKAGES) == 4
        import_names = [p[0] for p in TRAINING_PACKAGES]
        assert "peft" in import_names
        assert "transformers" in import_names


# ══════════════════════════════════════════════════════════════
# Security
# ══════════════════════════════════════════════════════════════


class TestSecurity:
    def test_training_capability(self):
        from familiar.core.security import SKILL_DIRECTORY_CAPABILITIES, Capability

        assert "training" in SKILL_DIRECTORY_CAPABILITIES
        assert Capability.WRITE_MEMORY in SKILL_DIRECTORY_CAPABILITIES["training"]


# ══════════════════════════════════════════════════════════════
# Bridge (new anchor types)
# ══════════════════════════════════════════════════════════════


class TestBridge:
    def test_anchor_consent_no_genesis(self):
        """anchor_consent returns empty string when no genesis exists."""
        from familiar.skills.training.bridge import anchor_consent

        with patch("familiar.skills.training.bridge._get_store") as mock_store:
            mock_store.return_value.get_genesis.return_value = None
            result = anchor_consent({"action": "grant", "user_id": "u1"})
            assert result == ""

    def test_anchor_erasure_no_genesis(self):
        """anchor_erasure returns empty string when no genesis exists."""
        from familiar.skills.training.bridge import anchor_erasure

        with patch("familiar.skills.training.bridge._get_store") as mock_store:
            mock_store.return_value.get_genesis.return_value = None
            result = anchor_erasure({"user_id": "u1", "records_deleted": 5})
            assert result == ""

    def test_anchor_training_run_no_genesis(self):
        """anchor_training_run returns empty string when no genesis exists."""
        from familiar.skills.training.bridge import anchor_training_run

        with patch("familiar.skills.training.bridge._get_store") as mock_store:
            mock_store.return_value.get_genesis.return_value = None
            result = anchor_training_run({"model": "test", "dataset": "test"})
            assert result == ""


# ══════════════════════════════════════════════════════════════
# Import Smoke Tests
# ══════════════════════════════════════════════════════════════


class TestImports:
    def test_import_pipeline(self):
        from familiar.skills.training import pipeline

        assert hasattr(pipeline, "PipelineConfig")
        assert hasattr(pipeline, "PipelineRunState")
        assert hasattr(pipeline, "run_pipeline")
        assert hasattr(pipeline, "get_pipeline_status")

    def test_import_consent(self):
        from familiar.core import consent

        assert hasattr(consent, "ConsentManager")
        assert hasattr(consent, "ConsentRecord")
        assert hasattr(consent, "ConsentScope")
        assert hasattr(consent, "DataClassification")

    def test_import_merkle(self):
        from familiar.core import merkle

        assert hasattr(merkle, "MerkleTree")
        assert hasattr(merkle, "batch_chain_blocks")

    def test_import_chain_anchor(self):
        from familiar.core import chain_anchor

        assert hasattr(chain_anchor, "L2Anchor")
        assert hasattr(chain_anchor, "LocalLedger")
        assert hasattr(chain_anchor, "ArbitrumAnchor")
        assert hasattr(chain_anchor, "SolanaAnchor")

    def test_import_dross(self):
        from familiar.core import dross

        assert hasattr(dross, "DrossLedger")
        assert hasattr(dross, "DrossLedgerEntry")

    def test_import_train_cli(self):
        from familiar.core import train_cli

        assert hasattr(train_cli, "cli_main")
        assert hasattr(train_cli, "create_parser")

    def test_import_coop(self):
        from familiar.skills.training import coop

        assert hasattr(coop, "CoopManager")
        assert hasattr(coop, "CoopManifest")
        assert hasattr(coop, "CoopMember")
        assert hasattr(coop, "CoopRules")

    def test_import_coop_transport(self):
        from familiar.skills.training import coop_transport

        assert hasattr(coop_transport, "CoopTransport")

    def test_import_model_training(self):
        from familiar.skills.training import model_training

        assert hasattr(model_training, "TrainingConfig")
        assert hasattr(model_training, "TrainingResult")
        assert hasattr(model_training, "TrainingRunner")

    def test_import_model_card(self):
        from familiar.skills.training import model_card

        assert hasattr(model_card, "generate_model_card")
        assert hasattr(model_card, "save_model_card")

    def test_import_model_package(self):
        from familiar.skills.training import model_package

        assert hasattr(model_package, "package_model")
        assert hasattr(model_package, "verify_model_package")

    def test_import_deps(self):
        from familiar.core import deps

        assert hasattr(deps, "TRAINING_PACKAGES")

    def test_import_paths(self):
        from familiar.core import paths

        assert hasattr(paths, "COOP_DIR")
        assert hasattr(paths, "DROSS_DIR")
