"""Tests for aura v0.3.0 features: audit, scan_cache, watcher, identity_card, quickstart."""

import json
import tempfile
import time
from pathlib import Path

import pytest

from aura.schema import (
    Confidence,
    ContextPack,
    Fact,
    FactType,
    PackMeta,
    Rule,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def pack_with_secrets() -> ContextPack:
    """A pack containing various secret patterns."""
    return ContextPack(
        name="dangerous",
        scope="development",
        facts=[
            Fact(key="aws.key", value="AKIAIOSFODNN7EXAMPLE", type=FactType.CONTEXT),
            Fact(key="github.token", value="ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij", type=FactType.CONTEXT),
            Fact(key="openai.key", value="sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG", type=FactType.CONTEXT),
            Fact(key="email", value="user@example.com", type=FactType.IDENTITY),
            Fact(key="db", value="postgres://admin:pass@10.0.0.5:5432/mydb", type=FactType.CONTEXT),
        ],
        meta=PackMeta(description="Pack with secrets for testing"),
    )


@pytest.fixture
def clean_pack() -> ContextPack:
    """A pack with no secrets."""
    return ContextPack(
        name="clean",
        scope="development",
        facts=[
            Fact(key="languages.primary", value=["Python", "TypeScript"], type=FactType.SKILL),
            Fact(key="editor", value="Cursor", type=FactType.PREFERENCE),
            Fact(key="style", value="Concise, no fluff", type=FactType.STYLE),
        ],
        rules=[Rule(instruction="Always use strict TypeScript", priority=8)],
        meta=PackMeta(description="Clean dev pack"),
    )


@pytest.fixture
def identity_pack() -> ContextPack:
    """A pack with identity facts for identity card tests."""
    return ContextPack(
        name="developer",
        scope="development",
        facts=[
            Fact(key="identity.name", value="Nok", type=FactType.IDENTITY),
            Fact(key="role", value="Software Engineer at Boston Scientific", type=FactType.IDENTITY),
            Fact(key="languages.primary", value=["Python", "TypeScript"], type=FactType.SKILL),
            Fact(key="frameworks", value=["FastAPI", "Next.js"], type=FactType.SKILL),
            Fact(key="editor", value="Cursor", type=FactType.PREFERENCE),
        ],
        rules=[
            Rule(instruction="Always use strict TypeScript", priority=8),
            Rule(instruction="Prefer functional patterns", priority=5),
        ],
        meta=PackMeta(description="Dev pack with identity"),
    )


# ===========================================================================
# FEATURE 1: Audit (Secret Detection)
# ===========================================================================
class TestAudit:

    def test_detects_aws_key(self, pack_with_secrets):
        from aura.audit import audit_packs, Severity

        report = audit_packs([pack_with_secrets])
        aws_findings = [f for f in report.findings if "AWS" in f.pattern_name]
        assert len(aws_findings) >= 1
        assert aws_findings[0].severity == Severity.CRITICAL

    def test_detects_github_token(self, pack_with_secrets):
        from aura.audit import audit_packs, Severity

        report = audit_packs([pack_with_secrets])
        gh_findings = [f for f in report.findings if "GitHub" in f.pattern_name]
        assert len(gh_findings) >= 1
        assert gh_findings[0].severity == Severity.CRITICAL

    def test_detects_anthropic_key(self, pack_with_secrets):
        from aura.audit import audit_packs, Severity

        report = audit_packs([pack_with_secrets])
        ant_findings = [f for f in report.findings if "Anthropic" in f.pattern_name]
        assert len(ant_findings) >= 1
        assert ant_findings[0].severity == Severity.CRITICAL

    def test_detects_email(self, pack_with_secrets):
        from aura.audit import audit_packs, Severity

        report = audit_packs([pack_with_secrets])
        email_findings = [f for f in report.findings if "Email" in f.pattern_name]
        assert len(email_findings) >= 1
        assert email_findings[0].severity == Severity.WARNING

    def test_detects_database_url(self, pack_with_secrets):
        from aura.audit import audit_packs, Severity

        report = audit_packs([pack_with_secrets])
        db_findings = [f for f in report.findings if "Database" in f.pattern_name]
        assert len(db_findings) >= 1
        assert db_findings[0].severity == Severity.CRITICAL

    def test_clean_pack_passes(self, clean_pack):
        from aura.audit import audit_packs

        report = audit_packs([clean_pack])
        assert report.critical_count == 0

    def test_is_clean_property(self, clean_pack, pack_with_secrets):
        from aura.audit import audit_packs

        clean_report = audit_packs([clean_pack])
        assert clean_report.is_clean

        dirty_report = audit_packs([pack_with_secrets])
        assert not dirty_report.is_clean

    def test_redact_removes_critical_secrets(self, pack_with_secrets):
        from aura.audit import redact_packs

        original_aws = next(f for f in pack_with_secrets.facts if f.key == "aws.key").value
        packs, count = redact_packs([pack_with_secrets])
        assert count > 0
        # AWS key should be redacted (value changed from original)
        aws_fact = next(f for f in packs[0].facts if f.key == "aws.key")
        assert aws_fact.value != original_aws
        assert len(aws_fact.value) < len(original_aws)

    def test_redact_preserves_non_secret_facts(self, pack_with_secrets):
        from aura.audit import redact_packs

        original_email = next(f for f in pack_with_secrets.facts if f.key == "email").value
        packs, _ = redact_packs([pack_with_secrets])
        email_fact = next(f for f in packs[0].facts if f.key == "email")
        # Email is WARNING not CRITICAL, so redact should NOT touch it
        assert email_fact.value == original_email

    def test_redact_value_preview(self):
        from aura.audit import _redact

        result = _redact("ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij")
        assert result.startswith("ghp_")
        assert "********" in result
        assert len(result) < 40  # Much shorter than original

    def test_scan_value_with_list(self):
        from aura.audit import scan_value

        findings = scan_value(
            ["AKIAIOSFODNN7EXAMPLE", "safe_value"],
            "test-pack", "mixed_key"
        )
        assert len(findings) >= 1

    def test_format_report_clean(self, clean_pack):
        from aura.audit import audit_packs, format_audit_report

        report = audit_packs([clean_pack])
        formatted = format_audit_report(report)
        assert "clean" in formatted.lower() or "All clean" in formatted

    def test_format_report_dirty(self, pack_with_secrets):
        from aura.audit import audit_packs, format_audit_report

        report = audit_packs([pack_with_secrets])
        formatted = format_audit_report(report)
        assert "CRITICAL" in formatted

    def test_scans_rules_too(self):
        from aura.audit import audit_packs

        pack = ContextPack(
            name="test", scope="development",
            rules=[Rule(instruction="Use key AKIAIOSFODNN7EXAMPLE for AWS", priority=5)],
            meta=PackMeta(description="test"),
        )
        report = audit_packs([pack])
        assert report.critical_count >= 1

    def test_scans_meta_description(self):
        from aura.audit import audit_packs

        pack = ContextPack(
            name="test", scope="development",
            meta=PackMeta(description="Deploy with ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"),
        )
        report = audit_packs([pack])
        assert report.critical_count >= 1

    def test_multiple_packs(self, clean_pack, pack_with_secrets):
        from aura.audit import audit_packs

        report = audit_packs([clean_pack, pack_with_secrets])
        # All findings should be from the dangerous pack
        for f in report.findings:
            if f.severity.value == "critical":
                assert f.pack == "dangerous"

    def test_detects_bearer_token(self):
        from aura.audit import scan_value, Severity

        findings = scan_value(
            "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
            "test", "auth_header"
        )
        bearer = [f for f in findings if "Bearer" in f.pattern_name]
        assert len(bearer) >= 1
        assert bearer[0].severity == Severity.CRITICAL

    def test_detects_private_key(self):
        from aura.audit import scan_value, Severity

        findings = scan_value(
            "-----BEGIN RSA PRIVATE KEY-----\nMIIE...",
            "test", "ssh_key"
        )
        pk = [f for f in findings if "Private Key" in f.pattern_name]
        assert len(pk) >= 1

    def test_detects_slack_token(self):
        from aura.audit import scan_value, Severity

        # Build token at runtime to avoid GitHub push protection blocking the commit
        slack_token = "xoxb" + "-1234567890-1234567890-" + "AbCdEfGhIjKlMnOpQrStUvWx"
        findings = scan_value(
            slack_token,
            "test", "slack"
        )
        slack = [f for f in findings if "Slack" in f.pattern_name]
        assert len(slack) >= 1

    def test_empty_packs_returns_clean(self):
        from aura.audit import audit_packs

        report = audit_packs([])
        assert report.is_clean
        assert report.total == 0


# ===========================================================================
# FEATURE 1b: Doctor integration with audit
# ===========================================================================
class TestDoctorAuditIntegration:

    def test_doctor_catches_secrets(self, pack_with_secrets):
        from aura.doctor import diagnose

        report = diagnose([pack_with_secrets])
        secret_issues = [i for i in report.issues if "Secret" in i.message or "secret" in i.message.lower()]
        assert len(secret_issues) >= 1

    def test_doctor_clean_no_secret_warnings(self, clean_pack):
        from aura.doctor import diagnose

        report = diagnose([clean_pack])
        secret_issues = [i for i in report.issues if "Secret" in i.message or "secret" in i.message.lower()]
        assert len(secret_issues) == 0


# ===========================================================================
# FEATURE 2: Scan Cache (Content Hashing)
# ===========================================================================
class TestScanCache:

    def test_hash_content_deterministic(self):
        from aura.scan_cache import hash_content

        h1 = hash_content("hello world")
        h2 = hash_content("hello world")
        assert h1 == h2

    def test_hash_content_different(self):
        from aura.scan_cache import hash_content

        h1 = hash_content("hello")
        h2 = hash_content("world")
        assert h1 != h2

    def test_hash_file(self, tmp_path):
        from aura.scan_cache import hash_file

        f = tmp_path / "test.txt"
        f.write_text("hello world")
        h = hash_file(f)
        assert h is not None
        assert len(h) == 64  # SHA-256 hex

    def test_hash_file_nonexistent(self, tmp_path):
        from aura.scan_cache import hash_file

        h = hash_file(tmp_path / "nope.txt")
        assert h is None

    def test_has_changed_first_time(self, tmp_path, monkeypatch):
        from aura.scan_cache import has_changed, _cache_path

        # Point cache to temp dir
        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        assert has_changed("git-identity", "abc123") is True

    def test_has_changed_same_hash(self, tmp_path, monkeypatch):
        from aura.scan_cache import has_changed, update_entry, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        update_entry("git-identity", "abc123")
        assert has_changed("git-identity", "abc123") is False

    def test_has_changed_different_hash(self, tmp_path, monkeypatch):
        from aura.scan_cache import has_changed, update_entry, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        update_entry("git-identity", "abc123")
        assert has_changed("git-identity", "xyz789") is True

    def test_update_cache_batch(self, tmp_path, monkeypatch):
        from aura.scan_cache import update_cache, _load_cache, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        update_cache({"src1": "hash1", "src2": "hash2", "src3": "hash3"})
        cache = _load_cache()
        assert len(cache["entries"]) == 3
        assert cache["last_full_scan"] is not None

    def test_get_changed_sources(self, tmp_path, monkeypatch):
        from aura.scan_cache import update_cache, get_changed_sources, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        update_cache({"a": "hash_a", "b": "hash_b"})
        changed = get_changed_sources({"a": "hash_a", "b": "new_hash", "c": "hash_c"})
        assert "a" not in changed  # Unchanged
        assert "b" in changed       # Changed
        assert "c" in changed       # New

    def test_get_cache_stats(self, tmp_path, monkeypatch):
        from aura.scan_cache import update_cache, get_cache_stats, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        update_cache({"x": "y"})
        stats = get_cache_stats()
        assert stats["total_entries"] == 1

    def test_clear_cache(self, tmp_path, monkeypatch):
        from aura.scan_cache import update_cache, clear_cache, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        update_cache({"x": "y"})
        assert cache_file.exists()
        clear_cache()
        assert not cache_file.exists()

    def test_corrupted_cache_handled(self, tmp_path, monkeypatch):
        from aura.scan_cache import _load_cache, _cache_path

        cache_file = tmp_path / "scan_cache.json"
        cache_file.write_text("NOT JSON {{{")
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        cache = _load_cache()
        assert cache["version"] == 1  # Returns clean default


# ===========================================================================
# FEATURE 2b: Scanner incremental mode
# ===========================================================================
class TestScannerIncremental:

    def test_scanner_has_incremental_flag(self):
        from aura.scanner import Scanner

        s = Scanner(incremental=True)
        assert s.incremental is True
        assert s._skipped == 0

    def test_scanner_non_incremental(self):
        from aura.scanner import Scanner

        s = Scanner(incremental=False)
        assert s.incremental is False

    def test_should_scan_always_true_when_not_incremental(self):
        from aura.scanner import Scanner

        s = Scanner(incremental=False)
        assert s._should_scan("anything", "content") is True

    def test_should_scan_first_time(self, tmp_path, monkeypatch):
        from aura.scanner import Scanner

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        s = Scanner(incremental=True)
        assert s._should_scan("new-source", "content") is True

    def test_should_scan_unchanged(self, tmp_path, monkeypatch):
        from aura.scan_cache import update_entry, hash_content, _cache_path
        from aura.scanner import Scanner

        cache_file = tmp_path / "scan_cache.json"
        monkeypatch.setattr("aura.scan_cache._cache_path", lambda: cache_file)

        # Pre-populate cache
        update_entry("git-identity", hash_content("John|john@example.com"))

        s = Scanner(incremental=True)
        result = s._should_scan("git-identity", "John|john@example.com")
        assert result is False
        assert s._skipped == 1


# ===========================================================================
# FEATURE 3: File Watcher
# ===========================================================================
class TestWatcher:

    def test_polling_watcher_creates(self, tmp_path):
        from aura.watcher import PollingWatcher

        callback_called = []
        watcher = PollingWatcher(tmp_path, lambda: callback_called.append(1), interval=0.1)
        assert watcher.watch_dir == tmp_path

    def test_polling_watcher_detects_change(self, tmp_path):
        from aura.watcher import PollingWatcher

        callback_count = []

        def on_change():
            callback_count.append(1)

        # Create initial file
        (tmp_path / "test.yaml").write_text("initial")

        watcher = PollingWatcher(tmp_path, on_change, interval=0.1)
        watcher.start()

        time.sleep(0.3)  # Let it take initial snapshot

        # Modify the file
        (tmp_path / "test.yaml").write_text("modified")

        time.sleep(0.5)  # Wait for detection
        watcher.stop()

        assert len(callback_count) >= 1

    def test_polling_watcher_ignores_non_yaml(self, tmp_path):
        from aura.watcher import PollingWatcher

        callback_count = []
        watcher = PollingWatcher(tmp_path, lambda: callback_count.append(1), interval=0.1)
        watcher.start()

        time.sleep(0.2)
        # Non-yaml snapshot taken

        # Create a .txt file — should not trigger
        (tmp_path / "readme.txt").write_text("hello")
        time.sleep(0.3)
        watcher.stop()

        # But the snapshot only tracks yaml, so a txt file change still appears
        # in the overall dict comparison. This tests internal behavior.
        # The real filter is in watchdog handler; polling watches all changes.

    def test_create_watcher_returns_tuple(self, tmp_path):
        from aura.watcher import create_watcher

        watcher, engine = create_watcher(lambda: None, tmp_path)
        assert engine in ("watchdog", "polling")

    def test_polling_watcher_start_stop(self, tmp_path):
        from aura.watcher import PollingWatcher

        watcher = PollingWatcher(tmp_path, lambda: None, interval=0.1)
        watcher.start()
        assert watcher._running is True
        watcher.stop()
        assert watcher._running is False

    def test_polling_watcher_detects_new_file(self, tmp_path):
        from aura.watcher import PollingWatcher

        callback_count = []
        watcher = PollingWatcher(tmp_path, lambda: callback_count.append(1), interval=0.1)
        watcher.start()
        time.sleep(0.2)

        (tmp_path / "new.yaml").write_text("new pack")
        time.sleep(0.3)
        watcher.stop()
        assert len(callback_count) >= 1

    def test_polling_watcher_detects_delete(self, tmp_path):
        from aura.watcher import PollingWatcher

        (tmp_path / "existing.yaml").write_text("data")

        callback_count = []
        watcher = PollingWatcher(tmp_path, lambda: callback_count.append(1), interval=0.1)
        watcher.start()
        time.sleep(0.2)

        (tmp_path / "existing.yaml").unlink()
        time.sleep(0.3)
        watcher.stop()
        assert len(callback_count) >= 1


# ===========================================================================
# FEATURE 4: Token Delivery — Identity Card
# ===========================================================================
class TestIdentityCard:

    def test_identity_card_returns_name(self, identity_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([identity_pack])
        assert "Nok" in result

    def test_identity_card_returns_role(self, identity_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([identity_pack])
        assert "Boston Scientific" in result

    def test_identity_card_returns_stack(self, identity_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([identity_pack])
        assert "Stack:" in result

    def test_identity_card_returns_rules(self, identity_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([identity_pack])
        assert "rules:" in result.lower() or "Key rules:" in result

    def test_identity_card_is_compact(self, identity_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([identity_pack])
        # Should be very short — under ~500 chars for ~100 tokens
        assert len(result) < 500

    def test_identity_card_empty_packs(self):
        from aura.mcp_server import _identity_card

        result = _identity_card([])
        assert "No user identity" in result or "quickstart" in result

    def test_identity_card_no_identity_facts(self, clean_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([clean_pack])
        # Should still return something useful (stack info)
        assert len(result) > 0

    def test_identity_card_multiple_packs(self, identity_pack, clean_pack):
        from aura.mcp_server import _identity_card

        result = _identity_card([identity_pack, clean_pack])
        assert "Nok" in result

    def test_identity_card_redacts_secrets(self):
        from aura.mcp_server import _identity_card

        pack = ContextPack(
            name="test", scope="development",
            facts=[
                Fact(key="identity.name", value="Test User", type=FactType.IDENTITY),
                Fact(key="role", value="Dev with key AKIAIOSFODNN7EXAMPLE", type=FactType.IDENTITY),
            ],
            meta=PackMeta(description="test"),
        )
        result = _identity_card([pack])
        assert "AKIAIOSFODNN7EXAMPLE" not in result

    def test_get_identity_card_tool_exists(self):
        from aura.mcp_server import TOOLS

        tool_names = [t["name"] for t in TOOLS]
        assert "get_identity_card" in tool_names

    def test_tool_hierarchy_exists(self):
        from aura.mcp_server import TOOLS

        tool_names = [t["name"] for t in TOOLS]
        # All 3 levels of token delivery
        assert "get_identity_card" in tool_names   # Level 1: ~50-100 tokens
        assert "get_user_profile" in tool_names     # Level 2: ~200-500 tokens
        assert "get_all_context" in tool_names      # Level 3: full dump

    def test_identity_card_shorter_than_profile(self, identity_pack):
        from aura.mcp_server import _compact_profile, _identity_card

        card = _identity_card([identity_pack])
        profile = _compact_profile([identity_pack])
        assert len(card) <= len(profile)


# ===========================================================================
# FEATURE 4b: MCP Server secret scrubbing
# ===========================================================================
class TestMCPSecretScrubbing:

    def test_scrub_secrets_removes_aws_key(self):
        from aura.mcp_server import _scrub_secrets

        result = _scrub_secrets("My key is AKIAIOSFODNN7EXAMPLE okay")
        assert "AKIAIOSFODNN7EXAMPLE" not in result
        assert "[REDACTED]" in result

    def test_scrub_secrets_preserves_clean_text(self):
        from aura.mcp_server import _scrub_secrets

        text = "I use Python and TypeScript"
        assert _scrub_secrets(text) == text

    def test_scrub_secrets_removes_github_token(self):
        from aura.mcp_server import _scrub_secrets

        result = _scrub_secrets("ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij")
        assert "ghp_" not in result

    def test_compact_profile_scrubs_secrets(self, pack_with_secrets):
        from aura.mcp_server import _compact_profile

        result = _compact_profile([pack_with_secrets])
        assert "AKIAIOSFODNN7EXAMPLE" not in result
        assert "ghp_" not in result


# ===========================================================================
# FEATURE 5: Quickstart integration (structural tests)
# ===========================================================================
class TestQuickstartStructure:

    def test_audit_module_importable(self):
        from aura.audit import audit_packs, redact_packs, format_audit_report
        assert callable(audit_packs)
        assert callable(redact_packs)
        assert callable(format_audit_report)

    def test_scan_cache_module_importable(self):
        from aura.scan_cache import (
            hash_content, hash_file, has_changed,
            update_entry, update_cache, get_changed_sources,
            get_cache_stats, clear_cache,
        )
        assert callable(hash_content)

    def test_watcher_module_importable(self):
        from aura.watcher import create_watcher, start_watching, PollingWatcher
        assert callable(create_watcher)
        assert callable(start_watching)

    def test_cli_audit_command_exists(self):
        from aura.cli import app
        # Typer uses callback.__name__ when name is not explicitly passed
        command_names = [cmd.name or cmd.callback.__name__ for cmd in app.registered_commands]
        assert "audit" in command_names

    def test_cli_serve_has_watch_flag(self):
        """Verify serve command accepts --watch."""
        from aura.cli import serve
        import inspect
        sig = inspect.signature(serve)
        assert "watch" in sig.parameters
