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

"""Tests for Sprint 5 — Tool Execution Middleware (credential pre-flight + retry)."""

from unittest.mock import MagicMock, patch

import pytest

from familiar.core.credential_health import (
    TOOL_CREDENTIAL_MAP,
    CredentialStatus,
    credential_preflight,
    credential_retry_on_auth_failure,
    ensure_credentials,
    get_required_credentials,
    invalidate_credential,
)
from familiar.core.credential_health import (
    _reset_for_testing as reset_health,
)
from familiar.core.self_correction import ErrorClass, classify_error


@pytest.fixture(autouse=True)
def clean_state(tmp_path, monkeypatch):
    """Reset credential health state and redirect status file."""
    reset_health()
    status_file = tmp_path / "credential_status.json"
    monkeypatch.setattr("familiar.core.credential_health._STATUS_FILE", status_file)
    yield
    reset_health()


# ── ErrorClass.CREDENTIAL_INVALID ───────────────────────────────────────────


class TestCredentialInvalidErrorClass:
    def test_enum_exists(self):
        assert ErrorClass.CREDENTIAL_INVALID == "credential_invalid"

    def test_classify_401(self):
        assert classify_error("401 Unauthorized") == ErrorClass.CREDENTIAL_INVALID

    def test_classify_token_expired(self):
        assert classify_error("Token expired") == ErrorClass.CREDENTIAL_INVALID

    def test_classify_needs_reauth(self):
        result = classify_error("GitHub needs re-authorization")
        assert result == ErrorClass.CREDENTIAL_INVALID

    def test_classify_visit_settings(self):
        result = classify_error("Visit Settings > Credentials to fix this")
        assert result == ErrorClass.CREDENTIAL_INVALID

    def test_not_retryable(self):
        from familiar.core.self_correction import is_retryable

        assert is_retryable(ErrorClass.CREDENTIAL_INVALID) is False


# ── TOOL_CREDENTIAL_MAP ────────────────────────────────────────────────────


class TestToolCredentialMap:
    def test_map_not_empty(self):
        assert len(TOOL_CREDENTIAL_MAP) > 0

    def test_github_prefix(self):
        assert get_required_credentials("github_list_issues") == ["github"]
        assert get_required_credentials("github_create_pr") == ["github"]

    def test_gcal_prefix(self):
        assert get_required_credentials("gcal_list_events") == ["google_workspace"]

    def test_gmail_prefix(self):
        assert get_required_credentials("gmail_send") == ["google_workspace"]

    def test_email_exact(self):
        assert get_required_credentials("send_email") == ["email"]

    def test_slack_prefix(self):
        assert get_required_credentials("slack_post_message") == ["slack"]

    def test_qb_prefix(self):
        assert get_required_credentials("qb_invoices") == ["quickbooks"]

    def test_no_match(self):
        assert get_required_credentials("remember") == []
        assert get_required_credentials("search_web") == []
        assert get_required_credentials("create_chart") == []


# ── credential_preflight ───────────────────────────────────────────────────


class TestCredentialPreflight:
    def test_no_credential_needed(self):
        ok, msg = credential_preflight("remember")
        assert ok is True
        assert msg == ""

    def test_configured_and_valid(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(True, "OK"),
        ):
            ok, msg = credential_preflight("github_list_issues")
        assert ok is True

    def test_not_configured(self, monkeypatch):
        monkeypatch.delenv("GITHUB_TOKEN", raising=False)
        ok, msg = credential_preflight("github_list_issues")
        assert ok is False
        assert "not configured" in msg.lower()
        assert "Settings" in msg

    def test_attention_needed(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_bad")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(False, "HTTP 401: Bad credentials"),
        ):
            ok, msg = credential_preflight("github_list_issues")
        assert ok is False
        assert "re-authorization" in msg.lower()
        assert "401" in msg


# ── credential_retry_on_auth_failure ────────────────────────────────────────


class TestCredentialRetryOnAuthFailure:
    def test_non_auth_error_no_retry(self):
        should_retry, msg = credential_retry_on_auth_failure(
            "github_list_issues", "Connection refused"
        )
        assert should_retry is False

    def test_no_credential_needed_no_retry(self):
        should_retry, msg = credential_retry_on_auth_failure("remember", "401 Unauthorized")
        assert should_retry is False

    def test_auth_error_refresh_succeeds(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(True, "Refreshed OK"),
        ):
            should_retry, msg = credential_retry_on_auth_failure(
                "github_list_issues", "HTTP 401: Bad credentials"
            )
        assert should_retry is True
        assert msg == ""

    def test_auth_error_refresh_fails(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_expired")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(False, "Still invalid"),
        ):
            should_retry, msg = credential_retry_on_auth_failure(
                "github_list_issues", "401 Unauthorized"
            )
        assert should_retry is False
        assert "re-authorization" in msg.lower()

    def test_token_expired_pattern(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(True, "OK"),
        ):
            should_retry, _ = credential_retry_on_auth_failure(
                "github_list_issues", "Token expired"
            )
        assert should_retry is True

    def test_access_denied_pattern(self, monkeypatch):
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(True, "OK"),
        ):
            should_retry, _ = credential_retry_on_auth_failure(
                "github_list_issues", "Access denied"
            )
        assert should_retry is True


# ── Integration: pre-flight blocks unconfigured tools ──────────────────────


class TestPreflightIntegration:
    def test_preflight_blocks_then_allows_after_config(self, monkeypatch):
        """Pre-flight blocks when unconfigured, then allows after setting env."""
        monkeypatch.delenv("GITHUB_TOKEN", raising=False)
        ok1, _ = credential_preflight("github_list_issues")
        assert ok1 is False

        monkeypatch.setenv("GITHUB_TOKEN", "ghp_new_token")
        invalidate_credential("github")
        with patch(
            "familiar.core.credential_registry.validate_github",
            return_value=(True, "OK"),
        ):
            ok2, _ = credential_preflight("github_list_issues")
        assert ok2 is True

    def test_tools_without_creds_unaffected(self):
        """Tools with no credential requirements pass through."""
        for tool_name in [
            "remember",
            "recall",
            "search_web",
            "create_chart",
            "list_tasks",
            "get_weather",
            "analyze_data",
        ]:
            ok, msg = credential_preflight(tool_name)
            assert ok is True, f"{tool_name} should pass preflight"
