"""Tests for consolidation adapters — Sprint 6.

Tests each adapter (analytics, credentials, email, workspace) with mocked data.
Tests PHI scrubbing, encryption enforcement, deduplication, temporal updates.
"""

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

import pytest

from familiar.core.consolidation_bridge import (
    ConsolidationBridge,
    SOURCE_ANALYTICS,
    SOURCE_CREDENTIALS,
    SOURCE_EMAIL,
    SOURCE_WORKSPACE,
)
from familiar.core.memory_tree import MemoryTree


@pytest.fixture
def bridge_with_mock_data(tmp_path):
    """Bridge with mock analytics.db and data files for testing."""
    data_dir = tmp_path / "data"
    data_dir.mkdir(exist_ok=True)

    tree = MemoryTree(db_path=tmp_path / "test_memory.db")
    bridge = ConsolidationBridge(tree=tree, data_dir=data_dir)
    # Enable PII detection for tests so PHI scrubbing works
    bridge._is_pii_enabled = lambda: True

    # Create mock analytics.db
    analytics_db = data_dir / "analytics.db"
    conn = sqlite3.connect(str(analytics_db))
    conn.execute("""
        CREATE TABLE daily_stats (
            date TEXT, user_id TEXT, channel TEXT, skill TEXT, model TEXT,
            event_count INTEGER, message_count INTEGER, tool_count INTEGER,
            error_count INTEGER, tokens_in INTEGER, tokens_out INTEGER,
            cost_usd REAL, total_latency_ms REAL
        )
    """)
    conn.execute("""
        CREATE TABLE events (
            id INTEGER PRIMARY KEY, timestamp TEXT, user_id TEXT, channel TEXT,
            event_type TEXT, skill TEXT, action TEXT, tokens_in INTEGER,
            tokens_out INTEGER, cost_usd REAL, latency_ms REAL, model TEXT,
            success INTEGER, error_message TEXT, metadata TEXT
        )
    """)
    # Insert test data
    conn.execute(
        "INSERT INTO daily_stats VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        ("2026-03-22", "test", "web", "chat", "test-model", 10, 8, 2, 1, 500, 200, 0.05, 100),
    )
    conn.execute(
        "INSERT INTO events VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        ("2026-03-22T10:00:00", "test", "web", "tool_call", "websearch", "search", 0, 0, 0, 0, "test-model", 0, "Test error occurred", "{}"),
    )
    conn.commit()
    conn.close()

    # Create mock credential_audit.db
    cred_db = data_dir / "credential_audit.db"
    conn = sqlite3.connect(str(cred_db))
    conn.execute("""
        CREATE TABLE credential_events (
            id INTEGER PRIMARY KEY, service_key TEXT, event_type TEXT,
            timestamp TEXT, details TEXT, source TEXT
        )
    """)
    conn.execute(
        "INSERT INTO credential_events VALUES (NULL, ?, ?, ?, ?, ?)",
        ("test_service", "failed", "2026-03-22T12:00:00", "Connection refused", "ensure_credentials"),
    )
    conn.execute(
        "INSERT INTO credential_events VALUES (NULL, ?, ?, ?, ?, ?)",
        ("test_service", "validated", "2026-03-22T12:05:00", "OK", "manual_test"),
    )
    conn.commit()
    conn.close()

    # Create mock email files (in data_dir which is already tmp_path/data)

    (data_dir / "email_engagement.json").write_text(json.dumps({
        "version": 1,
        "senders": {
            "vip@example.com": {"total_received": 10, "opened": 8, "replied": 5, "trashed": 0},
            "spam@junk.com": {"total_received": 50, "opened": 0, "replied": 0, "trashed": 20},
        },
    }))

    (data_dir / "email_sorted_cache.json").write_text(json.dumps({
        "emails": [{"id": "1"}] * 50,
        "summary": {"total": 50, "urgent": 2, "action": 10, "fyi": 38},
        "updated_at": "2026-03-22T16:00:00",
    }))

    (data_dir / "email_training_pairs.json").write_text(json.dumps({
        "version": 1,
        "pairs": [{"from_email": "a@b.com", "category": "Work"}] * 5,
        "stats": {"confirmed": 3, "corrected": 2, "high_confidence": 1},
    }))

    # Create mock creature + nonprofit data
    (data_dir / "creature.json").write_text(json.dumps({
        "job_class": "social_worker",
        "species_key": "owl",
        "exists": True,
    }))

    (data_dir / "nonprofit.json").write_text(json.dumps({
        "donors": [{"id": "d1", "name": "John Doe", "total_given": 500, "category": "individual"}],
        "grants": [{"id": "g1", "title": "Community Health", "status": "awarded", "amount": 10000, "agency": "HHS"}],
        "notes": [{"id": "n1", "content": "Case note for client Jane Smith SSN 123-45-6789", "category": "case_note"}],
        "compliance": [],
    }))

    (data_dir / "scheduled_tasks.json").write_text(json.dumps([
        {"name": "task1", "status": "active"},
        {"name": "task2", "status": "completed"},
    ]))

    return bridge, tmp_path


# ══════════════════════════════════════════════════════════
# ANALYTICS ADAPTER TESTS
# ══════════════════════════════════════════════════════════


def test_analytics_daily_usage(bridge_with_mock_data):
    """Analytics adapter creates daily usage memories."""
    bridge, tmp_path = bridge_with_mock_data
    if True:  # data_dir already set via constructor
        result = bridge._consolidate_analytics()
    assert result.errors == 0
    assert result.added > 0


def test_analytics_error_events(bridge_with_mock_data):
    """Analytics adapter creates error event memories."""
    bridge, tmp_path = bridge_with_mock_data
    if True:  # data_dir already set via constructor
        result = bridge._consolidate_analytics()
    # Search for the error
    results = bridge._tree.search("error", limit=10)
    error_mems = [r for r in results if "Error:" in r.title]
    assert len(error_mems) >= 1


def test_analytics_dedup_on_rerun(bridge_with_mock_data):
    """Analytics adapter doesn't duplicate on second run."""
    bridge, tmp_path = bridge_with_mock_data
    if True:  # data_dir already set via constructor
        r1 = bridge._consolidate_analytics()
        r2 = bridge._consolidate_analytics()
    assert r2.added == 0 or r2.noops > 0  # second run should be all noops/dedup


# ══════════════════════════════════════════════════════════
# CREDENTIAL ADAPTER TESTS
# ══════════════════════════════════════════════════════════


def test_credential_only_significant_events(bridge_with_mock_data):
    """Credential adapter only consolidates failed/updated/invalidated events."""
    bridge, tmp_path = bridge_with_mock_data
    if True:  # data_dir already set via constructor
        result = bridge._consolidate_credentials()
    # Should have the failure but not the routine validation
    cred_mems = bridge._tree.list_by_tag(SOURCE_CREDENTIALS, limit=20)
    titles = [m.title for m in cred_mems]
    assert any("failed" in t.lower() for t in titles)
    # Should NOT have routine "validated" events
    assert not any("validated" in t.lower() and "status" not in t.lower() for t in titles)


def test_credential_no_secrets_stored(bridge_with_mock_data):
    """Credential adapter never stores secret values."""
    bridge, tmp_path = bridge_with_mock_data
    if True:  # data_dir already set via constructor
        bridge._consolidate_credentials()
    # Check all credential memories for secret patterns
    cred_mems = bridge._tree.list_by_tag(SOURCE_CREDENTIALS, limit=20)
    for m in cred_mems:
        entry = bridge._tree.get(m.memory_id)
        assert "api_key" not in entry.body.lower() or "key" in entry.body.lower()  # status, not value
        assert "token=" not in entry.body.lower()
        assert "password=" not in entry.body.lower()


# ══════════════════════════════════════════════════════════
# EMAIL ADAPTER TESTS
# ══════════════════════════════════════════════════════════


def test_email_sort_stats(bridge_with_mock_data):
    """Email adapter creates sort stats memory."""
    bridge, tmp_path = bridge_with_mock_data
    # data_dir via bridge._data_dir
    if True:  # data_dir already set via constructor
        # Manually point to the data dir
        bridge._consolidate_sort_stats(bridge._data_dir, ConsolidationResult(source=SOURCE_EMAIL))
    results = bridge._tree.search("sort stats", limit=5)
    assert len(results) >= 1
    assert "50 sorted" in results[0].title


def test_email_training_stats(bridge_with_mock_data):
    """Email adapter creates training stats memory."""
    bridge, tmp_path = bridge_with_mock_data
    from familiar.core.consolidation_bridge import ConsolidationResult
    bridge._consolidate_training_stats(bridge._data_dir, ConsolidationResult(source=SOURCE_EMAIL))
    results = bridge._tree.search("training", limit=5)
    assert len(results) >= 1
    assert "5 pairs" in results[0].title


def test_email_sender_profile_threshold(bridge_with_mock_data):
    """Email adapter only consolidates senders with engagement > 1.0.
    Note: sender profiles require encryption (PHI). Without encryption,
    they are correctly skipped.
    """
    bridge, tmp_path = bridge_with_mock_data
    from familiar.core.consolidation_bridge import ConsolidationResult
    result = ConsolidationResult(source=SOURCE_EMAIL)
    bridge._consolidate_sender_profiles(bridge._data_dir, result)
    # vip@example.com has score (8*1 + 5*2)/10 = 1.8 → would be included
    # spam@junk.com has score 0/50 = 0 → excluded regardless
    # BUT require_encryption=True blocks storage without encryption key
    # So in test (no encryption), sender profiles are correctly skipped
    # Check audit log for skip_encryption entries
    log = bridge.get_log(limit=10)
    skipped = [l for l in log if l["action"] == "skip_encryption"]
    assert len(skipped) >= 1 or result.added >= 1


# ══════════════════════════════════════════════════════════
# WORKSPACE ADAPTER TESTS
# ══════════════════════════════════════════════════════════


def test_workspace_overview(bridge_with_mock_data):
    """Workspace adapter creates overview memory."""
    bridge, tmp_path = bridge_with_mock_data
    from familiar.core.consolidation_bridge import ConsolidationResult
    result = ConsolidationResult(source=SOURCE_WORKSPACE)
    bridge._consolidate_workspace_overview(bridge._data_dir, result)
    results = bridge._tree.search("workspace", limit=5)
    assert len(results) >= 1
    assert "Social Worker" in results[0].title


def test_workspace_grant_milestone(bridge_with_mock_data):
    """Workspace adapter consolidates grants with meaningful status.
    Social Worker job class requires encryption — without it, milestones
    are correctly skipped (HIPAA compliance).
    """
    bridge, tmp_path = bridge_with_mock_data
    from familiar.core.consolidation_bridge import ConsolidationResult
    result = ConsolidationResult(source=SOURCE_WORKSPACE)
    bridge._consolidate_workspace_data(bridge._data_dir, result)
    # Social worker data requires encryption — check either stored or skipped
    results = bridge._tree.search("grant", limit=5)
    milestones = [r for r in results if "Milestone" in r.title]
    log = bridge.get_log(limit=20)
    skipped = [l for l in log if l["action"] == "skip_encryption"]
    # Either milestones stored (encryption available) or correctly skipped
    assert len(milestones) >= 1 or len(skipped) >= 1


def test_workspace_phi_scrubbing_social_worker(bridge_with_mock_data):
    """Workspace adapter scrubs PHI from Social Worker data."""
    bridge, tmp_path = bridge_with_mock_data
    # data_dir via bridge._data_dir
    from familiar.core.consolidation_bridge import ConsolidationResult
    result = ConsolidationResult(source=SOURCE_WORKSPACE)
    # The nonprofit.json has a case note with a fake SSN
    bridge._consolidate_workspace_data(bridge._data_dir, result)
    # Check that the PHI was detected in audit
    log = bridge.get_log(limit=50)
    # The donor name "John Doe" should pass through (names aren't in regex fallback)
    # but the SSN "123-45-6789" in the notes should be caught if stored
    donor_mems = bridge._tree.search("donor", limit=5)
    if donor_mems:
        for m in donor_mems:
            entry = bridge._tree.get(m.memory_id)
            assert "123-45-6789" not in entry.body  # SSN must be scrubbed


def test_workspace_scheduled_tasks(bridge_with_mock_data):
    """Workspace adapter consolidates scheduled tasks summary."""
    bridge, tmp_path = bridge_with_mock_data
    # data_dir via bridge._data_dir
    from familiar.core.consolidation_bridge import ConsolidationResult
    result = ConsolidationResult(source=SOURCE_WORKSPACE)
    bridge._consolidate_scheduled_tasks(bridge._data_dir, result)
    results = bridge._tree.search("scheduled", limit=5)
    assert len(results) >= 1
    assert "2 total" in results[0].title


# ══════════════════════════════════════════════════════════
# PHI SCRUBBING GATE TESTS
# ══════════════════════════════════════════════════════════


def test_phi_gate_scrubs_multiple_types(bridge_with_mock_data):
    """PHI gate handles multiple PII types in one text."""
    bridge, _ = bridge_with_mock_data
    text = "Patient John at john@hospital.org, SSN 123-45-6789, phone 555-123-4567"
    scrubbed, found = bridge.phi_gate(text)
    assert found
    assert "[EMAIL]" in scrubbed
    assert "[SSN]" in scrubbed
    assert "[PHONE]" in scrubbed
    assert "john@hospital.org" not in scrubbed
    assert "123-45-6789" not in scrubbed


def test_phi_gate_preserves_clean_text(bridge_with_mock_data):
    """PHI gate doesn't modify clean text."""
    bridge, _ = bridge_with_mock_data
    text = "LLM Usage: 42 calls, 10000 tokens, $5.35 cost"
    scrubbed, found = bridge.phi_gate(text)
    assert not found
    assert scrubbed == text


# ══════════════════════════════════════════════════════════
# ENCRYPTION ENFORCEMENT TESTS
# ══════════════════════════════════════════════════════════


def test_encryption_blocks_phi_source_without_key(bridge_with_mock_data):
    """PHI-sensitive source is blocked when encryption unavailable."""
    bridge, _ = bridge_with_mock_data
    mid = bridge.store_with_audit(
        source=SOURCE_EMAIL,
        title="Sensitive sender data",
        body="PHI content here",
        require_encryption=True,
    )
    assert mid is None
    log = bridge.get_log(limit=1)
    assert log[0]["action"] == "skip_encryption"


def test_encryption_allows_non_phi_without_key(bridge_with_mock_data):
    """Non-PHI source works without encryption."""
    bridge, _ = bridge_with_mock_data
    mid = bridge.store_with_audit(
        source=SOURCE_ANALYTICS,
        title="Usage stats",
        body="10 calls, $0.05",
        require_encryption=False,
    )
    assert mid is not None


# ══════════════════════════════════════════════════════════
# TEMPORAL UPDATE TESTS
# ══════════════════════════════════════════════════════════


def test_temporal_update_invalidates_old(bridge_with_mock_data):
    """Updating a memory invalidates the old version."""
    bridge, _ = bridge_with_mock_data
    mid = bridge.store_with_audit(source=SOURCE_ANALYTICS, title="V1", body="Old data")
    bridge.update_with_audit(source=SOURCE_ANALYTICS, memory_id=mid, title="V2", body="New data")
    old = bridge._tree.get(mid)
    assert old.status == "invalidated"


def test_temporal_update_creates_new_version(bridge_with_mock_data):
    """Updating creates a new active version with supersedes edge."""
    bridge, _ = bridge_with_mock_data
    mid = bridge.store_with_audit(source=SOURCE_ANALYTICS, title="V1", body="Old")
    bridge.update_with_audit(source=SOURCE_ANALYTICS, memory_id=mid, title="V2", body="New")
    results = bridge._tree.search("V2", limit=5)
    active = [r for r in results if r.status == "active"]
    assert len(active) >= 1
    assert "New" in active[0].body


# ══════════════════════════════════════════════════════════
# HIPAA AUDIT REPORT TESTS
# ══════════════════════════════════════════════════════════


def test_hipaa_audit_report_structure(bridge_with_mock_data):
    """HIPAA audit report has required sections."""
    bridge, _ = bridge_with_mock_data
    # Run some consolidation first
    bridge.store_with_audit(source=SOURCE_ANALYTICS, title="Test", body="Data")
    report = bridge.generate_hipaa_audit_report()
    assert "encryption" in report
    assert "phi_scrubbing" in report
    assert "consolidation" in report
    assert "compliance" in report
    assert report["compliance"]["soft_delete_only"] is True
    assert report["compliance"]["audit_trail"] is True
    assert report["compliance"]["no_secrets_in_memory"] is True


def test_hipaa_audit_report_counts(bridge_with_mock_data):
    """HIPAA audit report shows correct operation counts."""
    bridge, _ = bridge_with_mock_data
    bridge.store_with_audit(source=SOURCE_ANALYTICS, title="Clean", body="No PHI here")
    bridge.store_with_audit(source=SOURCE_EMAIL, title="PHI: test@example.com", body="Clean body")
    report = bridge.generate_hipaa_audit_report()
    assert report["consolidation"]["total_operations"] == 2
    assert report["consolidation"]["consolidated_memories"] == 2
    assert report["phi_scrubbing"]["total_detections"] == 1


# ══════════════════════════════════════════════════════════
# FULL PIPELINE TEST
# ══════════════════════════════════════════════════════════


def test_full_consolidation_pipeline(bridge_with_mock_data):
    """Full consolidate_all runs all adapters without error."""
    bridge, tmp_path = bridge_with_mock_data
    if True:  # data_dir already set via constructor
        results = bridge.consolidate_all()
    assert all(r.errors == 0 for r in results.values())
    total_added = sum(r.added for r in results.values())
    assert total_added > 0

    # Verify audit trail is populated
    log = bridge.get_log(limit=100)
    assert len(log) > 0

    # Verify status is updated
    status = bridge.get_status()
    assert status.last_run is not None


# Need this import for individual adapter tests
from familiar.core.consolidation_bridge import ConsolidationResult
