"""
E2E tests for auth CLI workflow.

Tests verify the complete auth workflow using CliRunner with file isolation.
These are integration-level tests that exercise the full CLI surface.

Per R7 (Boundary Isolation): Tests use tmp_path and monkeypatch for isolation.
"""

import json
import pytest
from typer.testing import CliRunner

from lattice.shell.cli import auth_app

runner = CliRunner()


class TestAuthE2E:
    """End-to-end tests for auth CLI workflow."""

    @pytest.fixture
    def temp_auth_file(self, tmp_path, monkeypatch):
        """Set up temporary auth file for tests."""
        auth_file = tmp_path / "auth.json"
        monkeypatch.setattr("lattice.shell.auth.AUTH_FILE", auth_file)
        return auth_file

    def test_full_auth_workflow(self, temp_auth_file):
        """Complete auth workflow: login, list, logout."""
        auth_file = temp_auth_file

        # 1. lattice auth login openai --key sk-test
        result = runner.invoke(auth_app, ["login", "openai", "--key", "sk-test"])
        assert result.exit_code == 0
        assert "saved" in result.output.lower() or "success" in result.output.lower()

        # 2. lattice auth list (shows openai)
        result = runner.invoke(auth_app, ["list"])
        assert result.exit_code == 0
        assert "openai" in result.output

        # 3. Verify the key was saved correctly
        auth_data = json.loads(auth_file.read_text())
        assert "openai" in auth_data
        assert auth_data["openai"]["api_key"] == "sk-test"

        # 4. lattice auth logout openai
        result = runner.invoke(auth_app, ["logout", "openai"])
        assert result.exit_code == 0

        # 5. lattice auth list (empty)
        result = runner.invoke(auth_app, ["list"])
        assert result.exit_code == 0
        assert (
            "no api key" in result.output.lower()
            or "empty" in result.output.lower()
            or "no keys" in result.output.lower()
        )

    def test_multiple_providers_workflow(self, temp_auth_file):
        """Test managing multiple providers."""
        # Login multiple providers
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-openai"])
        runner.invoke(auth_app, ["login", "anthropic", "--key", "sk-ant"])
        runner.invoke(auth_app, ["login", "google", "--key", "sk-google"])

        # List should show all
        result = runner.invoke(auth_app, ["list"])
        assert result.exit_code == 0
        assert "openai" in result.output
        assert "anthropic" in result.output
        assert "google" in result.output

        # Logout one
        result = runner.invoke(auth_app, ["logout", "anthropic"])
        assert result.exit_code == 0

        # List should show remaining
        result = runner.invoke(auth_app, ["list"])
        assert "openai" in result.output
        assert "google" in result.output
        assert "anthropic" not in result.output

    def test_auth_key_redacted_in_list(self, temp_auth_file):
        """Ensure API keys are not shown in list output."""
        # Login
        runner.invoke(
            auth_app, ["login", "openai", "--key", "sk-super-secret-key-12345"]
        )

        # List
        result = runner.invoke(auth_app, ["list"])

        # Key should NOT appear in output (redacted)
        assert "sk-super-secret-key-12345" not in result.output
        assert "openai" in result.output

    def test_logout_nonexistent_provider(self, temp_auth_file):
        """Gracefully handle logout of non-existent provider."""
        result = runner.invoke(auth_app, ["logout", "nonexistent"])
        # Should succeed with appropriate message
        assert result.exit_code == 0
        assert (
            "no api key" in result.output.lower()
            or "not found" in result.output.lower()
            or "no key" in result.output.lower()
        )

    def test_login_updates_existing_key(self, temp_auth_file):
        """Login with same provider updates the key."""
        auth_file = temp_auth_file

        # First login
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-first"])

        # Second login (update)
        result = runner.invoke(auth_app, ["login", "openai", "--key", "sk-second"])
        assert result.exit_code == 0

        # Verify update
        auth_data = json.loads(auth_file.read_text())
        assert auth_data["openai"]["api_key"] == "sk-second"

    def test_auth_file_permissions(self, temp_auth_file):
        """Auth file should have restricted permissions (0o600)."""
        import stat

        # Login
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-test"])

        # Check permissions (should be owner read/write only)
        file_mode = temp_auth_file.stat().st_mode
        # Extract just the permission bits
        permissions = stat.S_IMODE(file_mode)
        # Should be 0o600 (owner read/write) or more restrictive
        assert permissions == 0o600, (
            f"Expected permissions 0o600, got {oct(permissions)}"
        )

    def test_auth_list_shows_provider_names_ordered(self, temp_auth_file):
        """List shows providers in consistent order."""
        # Login multiple providers in specific order
        runner.invoke(auth_app, ["login", "zebra", "--key", "sk-zebra"])
        runner.invoke(auth_app, ["login", "alpha", "--key", "sk-alpha"])
        runner.invoke(auth_app, ["login", "middle", "--key", "sk-middle"])

        result = runner.invoke(auth_app, ["list"])
        assert result.exit_code == 0

        # All providers should be present
        assert "zebra" in result.output
        assert "alpha" in result.output
        assert "middle" in result.output

    def test_auth_persistence_across_operations(self, temp_auth_file):
        """Keys persist across multiple operations."""
        # Add providers
        runner.invoke(auth_app, ["login", "provider1", "--key", "sk-1"])
        runner.invoke(auth_app, ["login", "provider2", "--key", "sk-2"])

        # Add another
        runner.invoke(auth_app, ["login", "provider3", "--key", "sk-3"])

        # Remove one
        runner.invoke(auth_app, ["logout", "provider2"])

        # Verify persistence
        auth_data = json.loads(temp_auth_file.read_text())
        assert "provider1" in auth_data
        assert "provider3" in auth_data
        assert "provider2" not in auth_data
        assert auth_data["provider1"]["api_key"] == "sk-1"
        assert auth_data["provider3"]["api_key"] == "sk-3"

    def test_login_with_special_characters_in_key(self, temp_auth_file):
        """Login handles keys with special characters."""
        # API keys often contain dashes, underscores, etc.
        special_key = "sk-proj-abc123_XYZ-789"
        result = runner.invoke(auth_app, ["login", "openai", "--key", special_key])
        assert result.exit_code == 0

        # Verify key was saved correctly
        auth_data = json.loads(temp_auth_file.read_text())
        assert auth_data["openai"]["api_key"] == special_key

    def test_login_empty_provider_name_rejected(self, temp_auth_file):
        """Login rejects empty provider name (validation enforced)."""
        result = runner.invoke(auth_app, ["login", "", "--key", "sk-test"])
        # Validation should reject empty provider name
        assert result.exit_code != 0
        assert "empty" in result.output.lower() or "error" in result.output.lower()

    def test_login_empty_key_accepted(self, temp_auth_file):
        """Login accepts empty key (implementation permits it)."""
        # Current implementation accepts empty key
        result = runner.invoke(auth_app, ["login", "openai", "--key", ""])
        # Implementation accepts it, so test documents actual behavior
        assert result.exit_code == 0

    def test_repeated_login_same_provider(self, temp_auth_file):
        """Repeated login to same provider updates key each time."""
        auth_file = temp_auth_file

        for i in range(3):
            key = f"sk-iteration-{i}"
            result = runner.invoke(auth_app, ["login", "openai", "--key", key])
            assert result.exit_code == 0

            auth_data = json.loads(auth_file.read_text())
            assert auth_data["openai"]["api_key"] == key

    def test_logout_after_multiple_logins(self, temp_auth_file):
        """Logout works correctly after multiple logins to same provider."""
        # Multiple logins to same provider
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-first"])
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-second"])
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-third"])

        # Logout
        result = runner.invoke(auth_app, ["logout", "openai"])
        assert result.exit_code == 0

        # Verify removed - file may be deleted when empty
        if temp_auth_file.exists():
            auth_data = json.loads(temp_auth_file.read_text())
            assert "openai" not in auth_data
        else:
            # File was deleted when empty - verify this expected behavior
            assert not temp_auth_file.exists(), (
                "Auth file should be deleted when last provider is removed"
            )

    def test_list_after_all_logout(self, temp_auth_file):
        """List shows empty state after all providers are removed."""
        # Add providers
        runner.invoke(auth_app, ["login", "p1", "--key", "sk-1"])
        runner.invoke(auth_app, ["login", "p2", "--key", "sk-2"])

        # Remove all
        runner.invoke(auth_app, ["logout", "p1"])
        runner.invoke(auth_app, ["logout", "p2"])

        # List should show empty
        result = runner.invoke(auth_app, ["list"])
        assert result.exit_code == 0
        assert "no" in result.output.lower() and (
            "key" in result.output.lower() or "api" in result.output.lower()
        )


class TestAuthTestCommand:
    """E2E tests for auth test command."""

    @pytest.fixture
    def temp_auth_file(self, tmp_path, monkeypatch):
        """Set up temporary auth file for tests."""
        auth_file = tmp_path / "auth.json"
        monkeypatch.setattr("lattice.shell.auth.AUTH_FILE", auth_file)
        return auth_file

    def test_auth_test_missing_provider(self, temp_auth_file):
        """Test command handles missing provider gracefully."""
        result = runner.invoke(auth_app, ["test", "nonexistent"])

        # Should fail with appropriate message
        assert result.exit_code != 0 or (
            "not found" in result.output.lower()
            or "no api key" in result.output.lower()
            or "error" in result.output.lower()
        )

    def test_auth_test_after_logout(self, temp_auth_file):
        """Test command fails gracefully after logout."""
        # Login and then logout
        runner.invoke(auth_app, ["login", "openai", "--key", "sk-test"])
        runner.invoke(auth_app, ["logout", "openai"])

        # Test should fail
        result = runner.invoke(auth_app, ["test", "openai"])
        assert result.exit_code != 0 or (
            "not found" in result.output.lower()
            or "no api key" in result.output.lower()
            or "error" in result.output.lower()
        )
