"""
Tests for CredentialProxy and service clients.
"""

from datetime import datetime, timezone

import pytest

from torivers_sdk.context.clients import (
    EmailMessage,
    GmailClient,
    GoogleSheetsClient,
    SlackChannel,
    SlackClient,
    get_client_class,
    list_supported_services,
)
from torivers_sdk.context.credentials import (
    CredentialError,
    CredentialNotAllowedError,
    CredentialNotConfiguredError,
    CredentialProxy,
)
from torivers_sdk.context.mocks import (
    MockCredentialProxy,
    MockGmailClient,
    MockGoogleSheetsClient,
    MockSlackClient,
)


class TestCredentialExceptions:
    """Test suite for credential exceptions."""

    def test_credential_error_is_base(self) -> None:
        """Test that CredentialError is the base exception."""
        assert issubclass(CredentialNotAllowedError, CredentialError)
        assert issubclass(CredentialNotConfiguredError, CredentialError)

    def test_credential_not_allowed_error(self) -> None:
        """Test CredentialNotAllowedError can be raised."""
        with pytest.raises(CredentialNotAllowedError):
            raise CredentialNotAllowedError("Service 'foo' not allowed")

    def test_credential_not_configured_error(self) -> None:
        """Test CredentialNotConfiguredError can be raised."""
        with pytest.raises(CredentialNotConfiguredError):
            raise CredentialNotConfiguredError("Credential not configured")


class TestCredentialProxy:
    """Test suite for CredentialProxy class."""

    def test_create_proxy(self) -> None:
        """Test creating a credential proxy."""
        proxy = CredentialProxy(["gmail", "slack"])
        assert proxy.is_service_allowed("gmail")
        assert proxy.is_service_allowed("slack")
        assert not proxy.is_service_allowed("unknown")

    def test_list_allowed_credentials(self) -> None:
        """Test listing allowed credentials."""
        proxy = CredentialProxy(["gmail", "google_sheets"])
        allowed = proxy.list_allowed_credentials()
        assert "gmail" in allowed
        assert "google_sheets" in allowed

    def test_get_client_not_allowed(self) -> None:
        """Test accessing a non-allowed service raises error."""
        proxy = CredentialProxy(["gmail"])

        with pytest.raises(CredentialNotAllowedError, match="not declared in manifest"):
            proxy.get_client("slack")

    def test_get_client_not_configured(self) -> None:
        """Test accessing unconfigured service raises error."""
        proxy = CredentialProxy(["gmail"])

        with pytest.raises(CredentialNotConfiguredError, match="not configured"):
            proxy.get_client("gmail")

    def test_has_credential_not_allowed(self) -> None:
        """Test has_credential returns False for non-allowed services."""
        proxy = CredentialProxy(["gmail"])
        assert not proxy.has_credential("slack")

    def test_has_credential_not_configured(self) -> None:
        """Test has_credential returns False when not configured."""
        proxy = CredentialProxy(["gmail"])
        assert not proxy.has_credential("gmail")

    def test_list_configured_credentials_empty(self) -> None:
        """Test listing configured credentials when none configured."""
        proxy = CredentialProxy(["gmail", "slack"])
        assert proxy.list_configured_credentials() == []


class TestServiceClientRegistry:
    """Test suite for service client registry."""

    def test_list_supported_services(self) -> None:
        """Test listing supported services."""
        services = list_supported_services()
        assert "gmail" in services
        assert "google_sheets" in services
        assert "slack" in services

    def test_get_client_class(self) -> None:
        """Test getting client classes."""
        assert get_client_class("gmail") == GmailClient
        assert get_client_class("google_sheets") == GoogleSheetsClient
        assert get_client_class("slack") == SlackClient
        assert get_client_class("unknown") is None


class TestServiceClientBase:
    """Test suite for ServiceClient base class."""

    def test_gmail_client_properties(self) -> None:
        """Test GmailClient properties."""
        client = GmailClient()
        assert client.service_name == "gmail"
        assert not client.is_authenticated()

    def test_sheets_client_properties(self) -> None:
        """Test GoogleSheetsClient properties."""
        client = GoogleSheetsClient()
        assert client.service_name == "google_sheets"
        assert not client.is_authenticated()

    def test_slack_client_properties(self) -> None:
        """Test SlackClient properties."""
        client = SlackClient()
        assert client.service_name == "slack"
        assert not client.is_authenticated()

    def test_clients_raise_not_configured(self) -> None:
        """Test that client methods raise CredentialNotConfiguredError without runtime context."""
        gmail = GmailClient()
        sheets = GoogleSheetsClient()
        slack = SlackClient()

        with pytest.raises(CredentialNotConfiguredError):
            gmail.send_email(["test@example.com"], "Subject", "Body")

        with pytest.raises(CredentialNotConfiguredError):
            sheets.get_range_values("spreadsheet-id", "Sheet1!A1:B2")

        with pytest.raises(CredentialNotConfiguredError):
            slack.send_message("#general", "Hello")


class TestMockGmailClient:
    """Test suite for MockGmailClient."""

    def test_create_mock_gmail(self) -> None:
        """Test creating mock Gmail client."""
        gmail = MockGmailClient()
        assert gmail.service_name == "gmail"
        assert gmail.is_authenticated()

    def test_send_email(self) -> None:
        """Test sending email."""
        gmail = MockGmailClient()
        message_id = gmail.send_email(
            to=["test@example.com"],
            subject="Test Subject",
            body="Test body",
        )

        assert message_id.startswith("mock-msg-")
        assert len(gmail.sent_emails) == 1
        assert gmail.sent_emails[0]["to"] == ["test@example.com"]
        assert gmail.sent_emails[0]["subject"] == "Test Subject"

    def test_list_emails(self) -> None:
        """Test listing emails."""
        gmail = MockGmailClient()
        gmail.add_mock_email(
            EmailMessage(
                id="msg-1",
                thread_id="thread-1",
                subject="Test Email",
                sender="sender@example.com",
                recipients=["recipient@example.com"],
                snippet="Hello...",
                date=datetime.now(timezone.utc),
                labels=["INBOX"],
            )
        )

        emails = gmail.list_emails()
        assert len(emails) == 1
        assert emails[0].subject == "Test Email"

    def test_list_emails_with_query(self) -> None:
        """Test listing emails with search query."""
        gmail = MockGmailClient()
        gmail.add_mock_emails(
            [
                EmailMessage(
                    id="msg-1",
                    thread_id="thread-1",
                    subject="Important Notice",
                    sender="sender@example.com",
                    recipients=["me@example.com"],
                    snippet="This is important",
                    date=datetime.now(timezone.utc),
                    labels=["INBOX"],
                ),
                EmailMessage(
                    id="msg-2",
                    thread_id="thread-2",
                    subject="Hello World",
                    sender="friend@example.com",
                    recipients=["me@example.com"],
                    snippet="Just saying hi",
                    date=datetime.now(timezone.utc),
                    labels=["INBOX"],
                ),
            ]
        )

        # Search by subject
        results = gmail.list_emails(query="important")
        assert len(results) == 1
        assert results[0].id == "msg-1"

    def test_get_email(self) -> None:
        """Test getting specific email."""
        gmail = MockGmailClient()
        gmail.add_mock_email(
            EmailMessage(
                id="msg-123",
                thread_id="thread-1",
                subject="Test",
                sender="test@example.com",
                recipients=["me@example.com"],
                snippet="...",
                date=datetime.now(timezone.utc),
                labels=[],
            )
        )

        email = gmail.get_email("msg-123")
        assert email.id == "msg-123"

        with pytest.raises(ValueError, match="not found"):
            gmail.get_email("nonexistent")

    def test_clear_mock_data(self) -> None:
        """Test clearing mock data."""
        gmail = MockGmailClient()
        gmail.send_email(["test@example.com"], "Subject", "Body")
        gmail.add_mock_email(
            EmailMessage(
                id="msg-1",
                thread_id="t1",
                subject="Test",
                sender="s@e.com",
                recipients=["r@e.com"],
                snippet="",
                date=datetime.now(timezone.utc),
                labels=[],
            )
        )

        gmail.clear_mock_data()
        assert len(gmail.sent_emails) == 0
        assert len(gmail.list_emails()) == 0


class TestMockGoogleSheetsClient:
    """Test suite for MockGoogleSheetsClient."""

    def test_create_mock_sheets(self) -> None:
        """Test creating mock Sheets client."""
        sheets = MockGoogleSheetsClient()
        assert sheets.service_name == "google_sheets"
        assert sheets.is_authenticated()

    def test_get_spreadsheet_info(self) -> None:
        """Test getting spreadsheet info."""
        sheets = MockGoogleSheetsClient()
        sheets.add_mock_spreadsheet("ss-123", "My Spreadsheet", {"Sheet1": []})

        info = sheets.get_spreadsheet_info("ss-123")
        assert info.spreadsheet_id == "ss-123"
        assert info.title == "My Spreadsheet"
        assert "Sheet1" in info.sheets

    def test_get_range_values(self) -> None:
        """Test getting range values."""
        sheets = MockGoogleSheetsClient()
        sheets.set_mock_data(
            "ss-123",
            "Sheet1",
            [["Name", "Age"], ["Alice", 30], ["Bob", 25]],
        )

        values = sheets.get_range_values("ss-123", "Sheet1!A1:B3")
        assert len(values) == 3
        assert values[0] == ["Name", "Age"]

    def test_update_range(self) -> None:
        """Test updating range values."""
        sheets = MockGoogleSheetsClient()
        cells_updated = sheets.update_range(
            "ss-123",
            "Sheet1!A1",
            [["Header1", "Header2"], ["Value1", "Value2"]],
        )

        assert cells_updated == 4
        values = sheets.get_range_values("ss-123", "Sheet1!A1:B2")
        assert values[0] == ["Header1", "Header2"]

    def test_append_rows(self) -> None:
        """Test appending rows."""
        sheets = MockGoogleSheetsClient()
        sheets.set_mock_data("ss-123", "Sheet1", [["Name"]])

        rows_added = sheets.append_rows("ss-123", "Sheet1", [["Alice"], ["Bob"]])
        assert rows_added == 2

        values = sheets.get_range_values("ss-123", "Sheet1")
        assert len(values) == 3

    def test_clear_range(self) -> None:
        """Test clearing range."""
        sheets = MockGoogleSheetsClient()
        sheets.set_mock_data("ss-123", "Sheet1", [["Data"]])

        sheets.clear_range("ss-123", "Sheet1")
        values = sheets.get_range_values("ss-123", "Sheet1")
        assert values == []


class TestMockSlackClient:
    """Test suite for MockSlackClient."""

    def test_create_mock_slack(self) -> None:
        """Test creating mock Slack client."""
        slack = MockSlackClient()
        assert slack.service_name == "slack"
        assert slack.is_authenticated()

    def test_send_message(self) -> None:
        """Test sending message."""
        slack = MockSlackClient()
        message = slack.send_message("#general", "Hello, World!")

        assert message.text == "Hello, World!"
        assert message.channel == "#general"
        assert len(slack.sent_messages) == 1

    def test_send_message_in_thread(self) -> None:
        """Test sending message in thread."""
        slack = MockSlackClient()
        parent = slack.send_message("#general", "Parent message")
        reply = slack.send_message("#general", "Reply", thread_ts=parent.ts)

        assert reply.thread_ts == parent.ts

    def test_get_channel(self) -> None:
        """Test getting channel info."""
        slack = MockSlackClient()
        slack.add_mock_channel(
            SlackChannel(
                id="C123",
                name="general",
                is_private=False,
                is_member=True,
            )
        )

        channel = slack.get_channel("#general")
        assert channel.id == "C123"
        assert channel.name == "general"

    def test_list_channels(self) -> None:
        """Test listing channels."""
        slack = MockSlackClient()
        slack.add_mock_channel(
            SlackChannel(id="C1", name="general", is_private=False, is_member=True)
        )
        slack.add_mock_channel(
            SlackChannel(id="C2", name="random", is_private=False, is_member=True)
        )

        channels = slack.list_channels()
        assert len(channels) == 2

    def test_add_reaction(self) -> None:
        """Test adding reaction."""
        slack = MockSlackClient()
        message = slack.send_message("#general", "React to this!")
        slack.add_reaction("#general", message.ts, "thumbsup")

        assert len(slack.added_reactions) == 1
        assert slack.added_reactions[0]["emoji"] == "thumbsup"


class TestMockCredentialProxy:
    """Test suite for MockCredentialProxy."""

    def test_create_mock_proxy(self) -> None:
        """Test creating mock credential proxy."""
        proxy = MockCredentialProxy(["gmail", "slack"])
        assert proxy.is_service_allowed("gmail")
        assert proxy.is_service_allowed("slack")

    def test_register_mock_client(self) -> None:
        """Test registering mock client."""
        proxy = MockCredentialProxy(["gmail"])
        gmail = MockGmailClient()
        proxy.register_mock_client("gmail", gmail)

        assert proxy.has_credential("gmail")
        client = proxy.get_client("gmail")
        assert isinstance(client, MockGmailClient)

    def test_register_not_allowed_raises(self) -> None:
        """Test registering non-allowed service raises error."""
        proxy = MockCredentialProxy(["gmail"])
        slack = MockSlackClient()

        with pytest.raises(CredentialNotAllowedError):
            proxy.register_mock_client("slack", slack)

    def test_unregister_mock_client(self) -> None:
        """Test unregistering mock client."""
        proxy = MockCredentialProxy(["gmail"])
        proxy.register_mock_client("gmail", MockGmailClient())

        assert proxy.has_credential("gmail")
        proxy.unregister_mock_client("gmail")
        assert not proxy.has_credential("gmail")

    def test_clear_all_mock_clients(self) -> None:
        """Test clearing all mock clients."""
        proxy = MockCredentialProxy(["gmail", "slack"])
        proxy.register_mock_client("gmail", MockGmailClient())
        proxy.register_mock_client("slack", MockSlackClient())

        proxy.clear_all_mock_clients()
        assert not proxy.has_credential("gmail")
        assert not proxy.has_credential("slack")

    def test_with_gmail_factory(self) -> None:
        """Test with_gmail factory method."""
        proxy = MockCredentialProxy.with_gmail()

        assert proxy.has_credential("gmail")
        gmail = proxy.get_client("gmail")
        assert isinstance(gmail, MockGmailClient)

    def test_with_google_sheets_factory(self) -> None:
        """Test with_google_sheets factory method."""
        proxy = MockCredentialProxy.with_google_sheets()

        assert proxy.has_credential("google_sheets")
        sheets = proxy.get_client("google_sheets")
        assert isinstance(sheets, MockGoogleSheetsClient)

    def test_with_slack_factory(self) -> None:
        """Test with_slack factory method."""
        proxy = MockCredentialProxy.with_slack()

        assert proxy.has_credential("slack")
        slack = proxy.get_client("slack")
        assert isinstance(slack, MockSlackClient)

    def test_with_all_services_factory(self) -> None:
        """Test with_all_services factory method."""
        proxy = MockCredentialProxy.with_all_services()

        assert proxy.has_credential("gmail")
        assert proxy.has_credential("google_sheets")
        assert proxy.has_credential("slack")


class TestIntegration:
    """Integration tests for credential system."""

    def test_automation_credential_flow(self) -> None:
        """Test typical automation credential flow."""
        # Setup mock proxy with Gmail
        proxy = MockCredentialProxy.with_gmail()
        gmail: MockGmailClient = proxy.get_client("gmail")  # type: ignore

        # Add some mock emails
        gmail.add_mock_email(
            EmailMessage(
                id="msg-1",
                thread_id="t1",
                subject="Important Update",
                sender="boss@company.com",
                recipients=["me@company.com"],
                snippet="Please review...",
                date=datetime.now(timezone.utc),
                labels=["INBOX", "IMPORTANT"],
            )
        )

        # Automation flow: check credential, list emails, send response
        if proxy.has_credential("gmail"):
            emails = gmail.list_emails(label="IMPORTANT")
            assert len(emails) == 1

            gmail.send_email(
                to=[emails[0].sender],
                subject=f"Re: {emails[0].subject}",
                body="Thanks for the update!",
            )

        assert len(gmail.sent_emails) == 1
        assert "Re: Important Update" in gmail.sent_emails[0]["subject"]

    def test_optional_credential_check(self) -> None:
        """Test checking optional credentials before use."""
        proxy = MockCredentialProxy(["gmail", "slack"])
        proxy.register_mock_client("gmail", MockGmailClient())
        # Slack not registered (optional, user didn't configure)

        # Safe way to use optional credentials
        if proxy.has_credential("slack"):
            slack = proxy.get_client("slack")
            slack.send_message("#general", "Hello")  # type: ignore
        else:
            # Fallback: use email instead
            gmail = proxy.get_client("gmail")
            gmail.send_email(["team@company.com"], "Notification", "Hello via email")  # type: ignore

        # Since Slack wasn't configured, email should have been sent
        gmail: MockGmailClient = proxy.get_client("gmail")  # type: ignore
        assert len(gmail.sent_emails) == 1
