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

"""Tests for credential registry (Sprint 0)."""

import os
from unittest.mock import MagicMock, patch

import pytest

from familiar.core.credential_registry import (
    CREDENTIAL_REGISTRY,
    CredentialSpec,
    CredentialType,
    ReauthType,
    Sensitivity,
    get_credential_spec,
    is_configured,
    list_credentials,
    validate,
)

# ── Enum Coverage ────────────────────────────────────────────────────────────


class TestEnums:
    def test_credential_type_values(self):
        assert set(CredentialType) == {
            CredentialType.API_KEY,
            CredentialType.OAUTH2,
            CredentialType.BASIC,
            CredentialType.BEARER,
            CredentialType.TOKEN_PAIR,
            CredentialType.SESSION,
            CredentialType.NONE,
        }

    def test_reauth_type_values(self):
        assert set(ReauthType) == {
            ReauthType.SIMPLE_TOKEN,
            ReauthType.OAUTH_REDIRECT,
            ReauthType.MULTI_FIELD,
            ReauthType.WIZARD_ONLY,
            ReauthType.NONE,
        }

    def test_sensitivity_values(self):
        assert set(Sensitivity) == {
            Sensitivity.LOW,
            Sensitivity.MEDIUM,
            Sensitivity.HIGH,
            Sensitivity.CRITICAL,
        }


# ── CredentialSpec ───────────────────────────────────────────────────────────


class TestCredentialSpec:
    def test_construction(self):
        spec = CredentialSpec(
            service_key="test",
            display_name="Test Service",
            category="apis",
            credential_type=CredentialType.API_KEY,
            env_vars=["TEST_KEY"],
            validate_fn=None,
            refresh_fn=None,
            reauth_type=ReauthType.SIMPLE_TOKEN,
            doc_url="https://example.com",
            doc_hint="Test hint",
            sensitivity=Sensitivity.LOW,
            check_interval=86400,
        )
        assert spec.service_key == "test"
        assert spec.scopes == []
        assert spec.required_packages == []

    def test_defaults(self):
        spec = CredentialSpec(
            service_key="x",
            display_name="X",
            category="apis",
            credential_type=CredentialType.NONE,
            env_vars=[],
            validate_fn=None,
            refresh_fn=None,
            reauth_type=ReauthType.NONE,
            doc_url="",
            doc_hint="",
            sensitivity=Sensitivity.LOW,
            check_interval=0,
        )
        assert spec.scopes == []
        assert spec.required_packages == []

    def test_to_dict(self):
        spec = CredentialSpec(
            service_key="test",
            display_name="Test",
            category="apis",
            credential_type=CredentialType.BEARER,
            env_vars=["TOKEN"],
            validate_fn="mod.func",
            refresh_fn=None,
            reauth_type=ReauthType.SIMPLE_TOKEN,
            doc_url="https://example.com",
            doc_hint="hint",
            sensitivity=Sensitivity.MEDIUM,
            check_interval=3600,
        )
        d = spec.to_dict()
        assert d["service_key"] == "test"
        assert d["credential_type"] == "bearer"
        assert d["has_validate"] is True
        assert d["has_refresh"] is False
        assert d["sensitivity"] == "medium"


# ── Registry Completeness ────────────────────────────────────────────────────


class TestRegistryCompleteness:
    def test_minimum_entry_count(self):
        assert len(CREDENTIAL_REGISTRY) >= 45

    def test_all_keys_unique(self):
        keys = list(CREDENTIAL_REGISTRY.keys())
        assert len(keys) == len(set(keys))

    def test_all_categories_present(self):
        categories = {s.category for s in CREDENTIAL_REGISTRY.values()}
        expected = {"llm", "integrations", "apis", "social", "selfhosted", "channels", "security"}
        assert expected.issubset(categories)

    def test_display_names_non_empty(self):
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert spec.display_name, f"{key} has empty display_name"

    def test_service_key_matches_dict_key(self):
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert key == spec.service_key, f"Key mismatch: {key} != {spec.service_key}"


# ── Registry Data Integrity ──────────────────────────────────────────────────


class TestRegistryIntegrity:
    def test_valid_credential_types(self):
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert isinstance(spec.credential_type, CredentialType), f"{key} bad type"

    def test_valid_reauth_types(self):
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert isinstance(spec.reauth_type, ReauthType), f"{key} bad reauth"

    def test_valid_sensitivity(self):
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert isinstance(spec.sensitivity, Sensitivity), f"{key} bad sensitivity"

    def test_valid_categories(self):
        valid = {
            "llm",
            "integrations",
            "apis",
            "social",
            "selfhosted",
            "channels",
            "security",
            "mesh",
        }
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert spec.category in valid, f"{key} bad category: {spec.category}"

    def test_check_intervals_non_negative(self):
        for key, spec in CREDENTIAL_REGISTRY.items():
            assert spec.check_interval >= 0, f"{key} negative check_interval"

    def test_none_types_have_no_required_env_vars(self):
        """NONE credential types shouldn't require env vars for auth."""
        for key, spec in CREDENTIAL_REGISTRY.items():
            if spec.credential_type == CredentialType.NONE and spec.reauth_type == ReauthType.NONE:
                # May still have optional env vars (e.g. OLLAMA_MODEL), so just check they exist
                assert isinstance(spec.env_vars, list), f"{key} env_vars not a list"

    def test_validate_fn_format(self):
        """validate_fn should be a dotted path or None."""
        for key, spec in CREDENTIAL_REGISTRY.items():
            if spec.validate_fn is not None:
                assert "." in spec.validate_fn, f"{key} validate_fn not dotted: {spec.validate_fn}"


# ── get_credential_spec ──────────────────────────────────────────────────────


class TestGetCredentialSpec:
    def test_known_key(self):
        spec = get_credential_spec("anthropic")
        assert spec is not None
        assert spec.display_name == "Anthropic (Claude)"

    def test_unknown_key(self):
        assert get_credential_spec("nonexistent") is None

    def test_returns_correct_type(self):
        spec = get_credential_spec("github")
        assert isinstance(spec, CredentialSpec)


# ── list_credentials ─────────────────────────────────────────────────────────


class TestListCredentials:
    def test_all_credentials(self):
        result = list_credentials()
        assert len(result) == len(CREDENTIAL_REGISTRY)

    def test_filter_by_category(self):
        result = list_credentials(category="llm")
        assert all(s.category == "llm" for s in result)
        assert len(result) >= 4

    def test_empty_category(self):
        result = list_credentials(category="nonexistent")
        assert result == []

    def test_configured_only(self, monkeypatch):
        monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test")
        result = list_credentials(configured_only=True)
        keys = [s.service_key for s in result]
        assert "anthropic" in keys

    def test_configured_only_excludes_unconfigured(self, monkeypatch):
        monkeypatch.delenv("HUBSPOT_ACCESS_TOKEN", raising=False)
        result = list_credentials(configured_only=True)
        keys = [s.service_key for s in result]
        assert "hubspot" not in keys


# ── is_configured ────────────────────────────────────────────────────────────


class TestIsConfigured:
    def test_unconfigured(self, monkeypatch):
        monkeypatch.delenv("GITHUB_TOKEN", raising=False)
        assert is_configured("github") is False

    def test_configured_via_env(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test123")
        assert is_configured("github") is True

    def test_none_type_always_configured(self):
        assert is_configured("ollama") is True
        assert is_configured("websearch") is True

    def test_unknown_service(self):
        assert is_configured("nonexistent") is False

    def test_multi_env_partial(self, monkeypatch):
        """If only some env vars are set, should be unconfigured."""
        monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-test")
        monkeypatch.delenv("SLACK_APP_TOKEN", raising=False)
        assert is_configured("slack") is False

    def test_multi_env_complete(self, monkeypatch):
        monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-test")
        monkeypatch.setenv("SLACK_APP_TOKEN", "xapp-test")
        assert is_configured("slack") is True

    def test_google_workspace_no_tokens(self, tmp_path, monkeypatch):
        monkeypatch.setattr("familiar.core.credential_registry.Path.home", lambda: tmp_path)
        assert is_configured("google_workspace") is False

    def test_google_workspace_with_token(self, tmp_path, monkeypatch):
        monkeypatch.setattr("familiar.core.credential_registry.Path.home", lambda: tmp_path)
        data_dir = tmp_path / ".familiar" / "data"
        data_dir.mkdir(parents=True)
        (data_dir / "google_token.json").write_text("{}")
        assert is_configured("google_workspace") is True


# ── validate ─────────────────────────────────────────────────────────────────


class TestValidate:
    def test_unknown_service(self):
        ok, msg = validate("nonexistent")
        assert ok is False
        assert "Unknown" in msg

    def test_no_validate_fn_unconfigured(self, monkeypatch):
        monkeypatch.delenv("GOOGLE_TRENDS_API_KEY", raising=False)
        ok, msg = validate("google_trends")
        assert ok is False
        assert "Not configured" in msg

    def test_no_validate_fn_configured(self, monkeypatch):
        monkeypatch.setenv("GOOGLE_TRENDS_API_KEY", "test-key")
        ok, msg = validate("google_trends")
        assert ok is True
        assert "credentials present" in msg

    def test_validate_dispatches(self, monkeypatch):
        """validate() should import and call the validate_fn."""
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(True, "Authenticated"),
        ):
            ok, msg = validate("github")
            assert ok is True

    def test_validate_handles_exception(self, monkeypatch):
        """validate() should catch exceptions in validate_fn."""
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with patch(
            "familiar.core.credential_registry.validate_github",
            side_effect=Exception("network error"),
        ):
            ok, msg = validate("github")
            assert ok is False
            assert "Validation error" in msg

    def test_validate_websearch_always_ok(self):
        ok, msg = validate("websearch")
        assert ok is True
        assert "DuckDuckGo" in msg

    def test_validate_dashboard_key_present(self, monkeypatch):
        monkeypatch.setenv("FAMILIAR_DASHBOARD_KEY", "a-long-secret-key-here")
        ok, msg = validate("dashboard_key")
        assert ok is True

    def test_validate_dashboard_key_too_short(self, monkeypatch):
        monkeypatch.setenv("FAMILIAR_DASHBOARD_KEY", "short")
        ok, msg = validate("dashboard_key")
        assert ok is False
        assert "too short" in msg

    def test_validate_dashboard_key_missing(self, monkeypatch):
        monkeypatch.delenv("FAMILIAR_DASHBOARD_KEY", raising=False)
        ok, msg = validate("dashboard_key")
        assert ok is False


# ── Validate Functions (unit tests) ──────────────────────────────────────────


class TestValidateFunctions:
    def test_validate_anthropic_missing_key(self, monkeypatch):
        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
        from familiar.core.credential_registry import validate_anthropic

        ok, msg = validate_anthropic()
        assert ok is False
        assert "not set" in msg

    def test_validate_github_missing_key(self, monkeypatch):
        monkeypatch.delenv("GITHUB_TOKEN", raising=False)
        from familiar.core.credential_registry import validate_github

        ok, msg = validate_github()
        assert ok is False
        assert "not set" in msg

    def test_validate_email_missing_fields(self, monkeypatch):
        monkeypatch.delenv("EMAIL_ADDRESS", raising=False)
        monkeypatch.delenv("EMAIL_PASSWORD", raising=False)
        monkeypatch.delenv("EMAIL_IMAP_SERVER", raising=False)
        monkeypatch.delenv("EMAIL_IMAP_PORT", raising=False)
        from familiar.core.credential_registry import validate_email

        ok, msg = validate_email()
        assert ok is False
        assert "Missing" in msg

    def test_validate_sms_missing_fields(self, monkeypatch):
        monkeypatch.delenv("TWILIO_ACCOUNT_SID", raising=False)
        monkeypatch.delenv("TWILIO_AUTH_TOKEN", raising=False)
        monkeypatch.delenv("TWILIO_PHONE_NUMBER", raising=False)
        from familiar.core.credential_registry import validate_sms

        ok, msg = validate_sms()
        assert ok is False
        assert "Missing" in msg

    def test_validate_reddit_missing_fields(self, monkeypatch):
        monkeypatch.delenv("REDDIT_CLIENT_ID", raising=False)
        monkeypatch.delenv("REDDIT_CLIENT_SECRET", raising=False)
        from familiar.core.credential_registry import validate_reddit

        ok, msg = validate_reddit()
        assert ok is False
        assert "not set" in msg

    def test_validate_bluesky_missing_fields(self, monkeypatch):
        monkeypatch.delenv("BLUESKY_HANDLE", raising=False)
        monkeypatch.delenv("BLUESKY_APP_PASSWORD", raising=False)
        from familiar.core.credential_registry import validate_bluesky

        ok, msg = validate_bluesky()
        assert ok is False
        assert "not set" in msg

    def test_validate_encryption_missing(self, monkeypatch):
        monkeypatch.delenv("FAMILIAR_ENCRYPTION_KEY", raising=False)
        from familiar.core.credential_registry import validate_encryption

        ok, msg = validate_encryption()
        assert ok is False
        assert "not set" in msg

    def test_validate_telegram_missing(self, monkeypatch):
        monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
        from familiar.core.credential_registry import validate_telegram

        ok, msg = validate_telegram()
        assert ok is False
        assert "not set" in msg

    def test_validate_discord_missing(self, monkeypatch):
        monkeypatch.delenv("DISCORD_BOT_TOKEN", raising=False)
        from familiar.core.credential_registry import validate_discord

        ok, msg = validate_discord()
        assert ok is False
        assert "not set" in msg

    def test_validate_pihole_missing_url(self, monkeypatch):
        monkeypatch.delenv("PIHOLE_URL", raising=False)
        from familiar.core.credential_registry import validate_pihole

        ok, msg = validate_pihole()
        assert ok is False
        assert "not set" in msg
