"""Tests for authentication ordering in item commands.

These tests verify that auth errors surface before project configuration errors,
as specified in issue #190.
"""

from __future__ import annotations

import importlib
from unittest.mock import patch

import pytest
from typer.testing import CliRunner

from cli.commands._auth import NOT_SIGNED_IN_MESSAGE, require_auth
from cli.config import CLIConfig
from cli.exceptions import CLIError, KeyringFailure
from cli.keychain import AuthSource

runner = CliRunner()


def _reload_cli_module():
    """Reload cli.main to get a fresh context."""
    import cli.main as cli_main

    return importlib.reload(cli_main)


class TestRequireAuth:
    """Tests for the require_auth helper function."""

    def test_mock_mode_bypasses_auth(self):
        """Mock mode should not require authentication."""
        config = CLIConfig(use_mock_client=True)
        # Should not raise
        require_auth(config)

    def test_raises_when_no_api_key(self):
        """Should raise CLIError when no API key is set."""
        config = CLIConfig(use_mock_client=False, api_base_url="https://api.example.com")
        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            with pytest.raises(CLIError) as exc_info:
                require_auth(config)
            assert NOT_SIGNED_IN_MESSAGE in str(exc_info.value)

    def test_raises_on_keyring_failure(self):
        """Should convert KeyringFailure to CLIError with auth message."""
        config = CLIConfig(use_mock_client=False, api_base_url="https://api.example.com")
        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            side_effect=KeyringFailure("Keyring unavailable"),
        ):
            with pytest.raises(CLIError) as exc_info:
                require_auth(config)
            assert NOT_SIGNED_IN_MESSAGE in str(exc_info.value)

    def test_passes_when_api_key_exists(self):
        """Should not raise when API key is available."""
        config = CLIConfig(use_mock_client=False, api_base_url="https://api.example.com")
        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=("ctx_test_key", AuthSource.KEYCHAIN),
        ):
            # Should not raise
            require_auth(config)


def _get_error_message(result) -> str:
    """Extract error message from result output or exception."""
    if result.output:
        return result.output
    if result.exception:
        return str(result.exception)
    return ""


class TestAuthBeforeProjectError:
    """Tests that auth errors appear before project configuration errors."""

    def test_ls_shows_auth_error_before_project_error(self, tmp_path, monkeypatch):
        """ctxme ls should show auth error when not signed in, even without project."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            result = runner.invoke(cli_main.app, ["ls"])

        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "Not signed in" in error_msg

    def test_ls_errors_when_authenticated_no_project(self, tmp_path, monkeypatch):
        """ctxme ls should error when authenticated but no project and no --all.

        Per issue #208 precedence rules, when no --project or --all flag is provided
        and no default project is set, an error should be raised.
        """
        from cli.config import CLIConfig, ConfigStore

        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        store = ConfigStore(base_dir=tmp_path)
        store.save(CLIConfig(use_mock_client=True))  # Mock mode, no default project

        result = runner.invoke(cli_main.app, ["ls"])

        # Should error instructing user to specify --project or --all
        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "No project specified" in error_msg
        assert "--project" in error_msg or "--all" in error_msg

    def test_get_shows_auth_error_before_project_error(self, tmp_path, monkeypatch):
        """ctxme get should show auth error when not signed in."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            result = runner.invoke(cli_main.app, ["get", "some-item"])

        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "Not signed in" in error_msg

    def test_put_shows_auth_error_before_project_error(self, tmp_path, monkeypatch):
        """ctxme put should show auth error when not signed in."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            result = runner.invoke(cli_main.app, ["put", "some-item", "--data", "test"])

        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "Not signed in" in error_msg

    def test_remove_shows_auth_error_before_project_error(self, tmp_path, monkeypatch):
        """ctxme remove should show auth error when not signed in."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            result = runner.invoke(cli_main.app, ["remove", "some-item", "--force"])

        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "Not signed in" in error_msg

    def test_search_shows_auth_error_before_project_error(self, tmp_path, monkeypatch):
        """ctxme search should show auth error when not signed in."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            result = runner.invoke(cli_main.app, ["search", "query"])

        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "Not signed in" in error_msg

    def test_search_all_shows_auth_error(self, tmp_path, monkeypatch):
        """ctxme search --all should show auth error when not signed in."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        with patch(
            "cli.commands._auth.get_api_key_with_env_override",
            return_value=(None, AuthSource.KEYCHAIN),
        ):
            result = runner.invoke(cli_main.app, ["search", "query", "--all"])

        assert result.exit_code != 0
        error_msg = _get_error_message(result)
        assert "Not signed in" in error_msg


class TestMockModeBypassesAuth:
    """Tests that mock mode continues to work without authentication."""

    def test_ls_works_in_mock_mode_without_auth(self, tmp_path, monkeypatch):
        """ctxme ls should work in mock mode even without API key."""
        import json

        # Set up config with mock mode enabled
        config_dir = tmp_path / "config"
        config_dir.mkdir()
        config_file = config_dir / "config.json"
        config_file.write_text(
            json.dumps(
                {
                    "use_mock_client": True,
                    "default_project": "test-project",
                }
            )
        )
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(config_dir))
        cli_main = _reload_cli_module()

        # No API key mocking needed - mock mode should bypass auth
        result = runner.invoke(cli_main.app, ["ls"])

        # Should not show auth error (may show empty list or mock data)
        assert "Not signed in" not in result.output


class TestArgumentValidationBeforeAuth:
    """Tests that argument validation errors still surface before auth."""

    def test_get_mutually_exclusive_args_error_before_auth(self, tmp_path, monkeypatch):
        """Argument validation should happen before auth check."""
        monkeypatch.setenv("CTXME_CONFIG_DIR", str(tmp_path))
        cli_main = _reload_cli_module()

        # Don't mock get_api_key - we want to see if arg validation happens first
        result = runner.invoke(cli_main.app, ["get", "item", "--stdout", "--dest", "file.txt"])

        # Should show argument error, not auth error
        assert result.exit_code != 0
        assert "mutually exclusive" in result.output.lower() or "cannot" in result.output.lower()
