# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for Sprint 7 — Credential Audit Log + Per-Skill Scoping."""

import pytest

from familiar.core.credential_audit import (
    SKILL_CREDENTIAL_MAP,
    CredentialEvent,
    CredentialEventType,
    check_skill_credential_access,
    get_credential_events,
    get_event_summary,
    get_skill_credentials,
    log_credential_event,
)
from familiar.core.credential_audit import (
    _reset_for_testing as reset_audit,
)


@pytest.fixture(autouse=True)
def clean_db(tmp_path, monkeypatch):
    """Use a temp DB for each test."""
    reset_audit()
    tmp_db = tmp_path / "test_audit.db"
    monkeypatch.setattr("familiar.core.credential_audit._DB_PATH", tmp_db)
    yield tmp_db
    reset_audit()


# ── CredentialEventType enum ─────────────────────────────────────────────


class TestCredentialEventType:
    def test_all_types_exist(self):
        expected = [
            "validated",
            "refreshed",
            "updated",
            "failed",
            "accessed",
            "invalidated",
            "created",
            "deleted",
            "preflight_blocked",
            "retry_refresh",
        ]
        for t in expected:
            assert CredentialEventType(t) == t

    def test_enum_is_string(self):
        assert isinstance(CredentialEventType.VALIDATED, str)
        assert CredentialEventType.VALIDATED == "validated"


# ── log_credential_event ─────────────────────────────────────────────────


class TestLogCredentialEvent:
    def test_log_returns_event(self, clean_db):
        event = log_credential_event(
            "github",
            CredentialEventType.VALIDATED,
            details="OK",
            source="test",
            db_path=clean_db,
        )
        assert isinstance(event, CredentialEvent)
        assert event.service_key == "github"
        assert event.event_type == "validated"
        assert event.details == "OK"
        assert event.source == "test"
        assert event.id  # UUID assigned
        assert event.timestamp  # ISO timestamp assigned

    def test_log_persists_to_db(self, clean_db):
        log_credential_event(
            "slack",
            CredentialEventType.FAILED,
            details="Token expired",
            source="startup",
            db_path=clean_db,
        )

        events = get_credential_events(db_path=clean_db)
        assert len(events) == 1
        assert events[0].service_key == "slack"
        assert events[0].event_type == "failed"

    def test_log_multiple_events(self, clean_db):
        for i in range(5):
            log_credential_event(
                "github",
                CredentialEventType.VALIDATED,
                details=f"attempt {i}",
                source="test",
                db_path=clean_db,
            )
        events = get_credential_events(db_path=clean_db)
        assert len(events) == 5


# ── get_credential_events ────────────────────────────────────────────────


class TestGetCredentialEvents:
    def test_filter_by_service_key(self, clean_db):
        log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)
        log_credential_event("slack", CredentialEventType.FAILED, db_path=clean_db)

        events = get_credential_events(service_key="github", db_path=clean_db)
        assert len(events) == 1
        assert events[0].service_key == "github"

    def test_filter_by_event_type(self, clean_db):
        log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)
        log_credential_event("github", CredentialEventType.FAILED, db_path=clean_db)

        events = get_credential_events(event_type="failed", db_path=clean_db)
        assert len(events) == 1
        assert events[0].event_type == "failed"

    def test_filter_by_since(self, clean_db):
        log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)

        # "since" far in the future should return nothing
        events = get_credential_events(since="2099-01-01T00:00:00", db_path=clean_db)
        assert len(events) == 0

        # "since" far in the past should return everything
        events = get_credential_events(since="2000-01-01T00:00:00", db_path=clean_db)
        assert len(events) == 1

    def test_limit(self, clean_db):
        for _ in range(10):
            log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)

        events = get_credential_events(limit=3, db_path=clean_db)
        assert len(events) == 3

    def test_newest_first(self, clean_db):
        log_credential_event(
            "github", CredentialEventType.VALIDATED, details="first", db_path=clean_db
        )
        log_credential_event(
            "github", CredentialEventType.FAILED, details="second", db_path=clean_db
        )

        events = get_credential_events(db_path=clean_db)
        assert events[0].details == "second"
        assert events[1].details == "first"

    def test_empty_db(self, clean_db):
        events = get_credential_events(db_path=clean_db)
        assert events == []


# ── get_event_summary ────────────────────────────────────────────────────


class TestGetEventSummary:
    def test_summary_empty(self, clean_db):
        summary = get_event_summary(db_path=clean_db)
        assert summary["total"] == 0
        assert summary["by_type"] == {}

    def test_summary_with_events(self, clean_db):
        log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)
        log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)
        log_credential_event("github", CredentialEventType.FAILED, db_path=clean_db)

        summary = get_event_summary(db_path=clean_db)
        assert summary["total"] == 3
        assert summary["by_type"]["validated"] == 2
        assert summary["by_type"]["failed"] == 1

    def test_summary_filtered_by_service(self, clean_db):
        log_credential_event("github", CredentialEventType.VALIDATED, db_path=clean_db)
        log_credential_event("slack", CredentialEventType.FAILED, db_path=clean_db)

        summary = get_event_summary(service_key="github", db_path=clean_db)
        assert summary["total"] == 1


# ── SKILL_CREDENTIAL_MAP ─────────────────────────────────────────────────


class TestSkillCredentialMap:
    def test_map_not_empty(self):
        assert len(SKILL_CREDENTIAL_MAP) > 0

    def test_github_skill(self):
        assert get_skill_credentials("github") == ["github"]

    def test_email_skill(self):
        assert get_skill_credentials("email") == ["email"]

    def test_calendar_skill(self):
        assert get_skill_credentials("calendar") == ["google_workspace"]

    def test_unknown_skill(self):
        assert get_skill_credentials("nonexistent_skill") == []

    def test_no_credential_skills(self):
        assert get_skill_credentials("documents") == []
        assert get_skill_credentials("charts") == []


# ── check_skill_credential_access ─────────────────────────────────────────


class TestCheckSkillCredentialAccess:
    def test_allowed_access(self):
        assert check_skill_credential_access("github", "github") is True

    def test_denied_access(self):
        assert check_skill_credential_access("github", "slack") is False

    def test_unknown_skill_allows(self):
        """Unknown skills defer to TOOL_CREDENTIAL_MAP — don't block."""
        assert check_skill_credential_access("totally_unknown", "github") is True

    def test_no_cred_skill_denies(self):
        """Skills with empty cred list can't access any credential."""
        assert check_skill_credential_access("charts", "github") is False


# ── API: /api/credentials/audit ───────────────────────────────────────────


@pytest.fixture
def dashboard_client(monkeypatch):
    """Create a Flask test client with auth configured."""
    monkeypatch.setenv("FAMILIAR_DASHBOARD_KEY", "test-key-for-auth")
    from familiar.dashboard.app import app

    app.config["TESTING"] = True
    with app.test_client() as client:
        yield client


def _auth_headers(post=False):
    h = {"X-API-Key": "test-key-for-auth"}
    if post:
        h["Origin"] = "http://localhost:5000"
    return h


class TestCredentialAuditEndpoint:
    def test_empty_audit(self, dashboard_client):
        resp = dashboard_client.get("/api/credentials/audit", headers=_auth_headers())
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["count"] == 0
        assert data["events"] == []

    def test_with_events(self, dashboard_client, clean_db):
        # Log directly to default DB (not tmp) since the endpoint uses default
        log_credential_event("github", CredentialEventType.VALIDATED, details="OK", source="test")

        resp = dashboard_client.get("/api/credentials/audit", headers=_auth_headers())
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["count"] >= 1

    def test_filter_by_service(self, dashboard_client, clean_db):
        log_credential_event("github", CredentialEventType.VALIDATED, source="test")
        log_credential_event("slack", CredentialEventType.FAILED, source="test")

        resp = dashboard_client.get(
            "/api/credentials/audit?service_key=github", headers=_auth_headers()
        )
        data = resp.get_json()
        assert all(e["service_key"] == "github" for e in data["events"])
