"""
Unit tests for resolve_api_key function.

Tests verify the public API of lattice.shell.config.resolve_api_key using
file isolation via tmp_path and monkeypatch fixtures.

Per R7 (Boundary Isolation): Unit tests mock the filesystem via monkeypatch.

Specification:
- None -> Success(None)
- 'sk-...' (direct) -> Success('sk-...')
- '{env:VAR}' -> Success(os.environ.get('VAR')) or Failure
- '{file:/path}' -> Success(file content) or Failure
- '{auth:provider}' -> Success(auth[provider]['api_key']) or Failure
- Invalid syntax -> Failure
"""

import pytest
from returns.result import Failure, Success

from lattice.shell.config import resolve_api_key


class TestResolveApiKeyNone:
    """Test None input handling."""

    def test_resolve_api_key_none_returns_success_none(self):
        """None input returns Success(None)."""
        result = resolve_api_key(None)
        assert isinstance(result, Success)
        assert result.unwrap() is None


class TestResolveApiKeyDirectPassthrough:
    """Test direct key passthrough."""

    def test_resolve_api_key_direct_passthrough(self):
        """Direct key returns Success with the key unchanged."""
        result = resolve_api_key("sk-test-key")
        assert isinstance(result, Success)
        assert result.unwrap() == "sk-test-key"

    def test_resolve_api_key_direct_any_string(self):
        """Any non-variable-syntax string passes through."""
        result = resolve_api_key("my-custom-key-12345")
        assert isinstance(result, Success)
        assert result.unwrap() == "my-custom-key-12345"

    def test_resolve_api_key_empty_string_returns_success_empty(self):
        """Empty string returns Success with empty string."""
        result = resolve_api_key("")
        assert isinstance(result, Success)
        assert result.unwrap() == ""


class TestResolveApiKeyEnvVar:
    """Test {env:VAR} variable resolution."""

    def test_resolve_api_key_env_var_success(self, monkeypatch):
        """{env:VAR} resolution returns env var value."""
        monkeypatch.setenv("MY_API_KEY", "secret-key-123")
        result = resolve_api_key("{env:MY_API_KEY}")
        assert isinstance(result, Success)
        assert result.unwrap() == "secret-key-123"

    def test_resolve_api_key_env_var_missing_returns_failure(self, monkeypatch):
        """{env:VAR} with missing env var returns Failure."""
        monkeypatch.delenv("MISSING_KEY", raising=False)
        result = resolve_api_key("{env:MISSING_KEY}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_env_var_empty_value(self, monkeypatch):
        """{env:VAR} with empty string value returns Success with empty."""
        monkeypatch.setenv("EMPTY_VAR", "")
        result = resolve_api_key("{env:EMPTY_VAR}")
        # Per spec: os.environ.get('VAR') returns empty string
        assert isinstance(result, Success)
        assert result.unwrap() == ""


class TestResolveApiKeyFile:
    """Test {file:/path} variable resolution."""

    def test_resolve_api_key_file_success(self, tmp_path):
        """{file:/path} resolution returns file content."""
        key_file = tmp_path / "api_key.txt"
        key_file.write_text("file-api-key")
        result = resolve_api_key(f"{{file:{key_file}}}")
        assert isinstance(result, Success)
        assert result.unwrap() == "file-api-key"

    def test_resolve_api_key_file_with_whitespace(self, tmp_path):
        """{file:/path} strips whitespace from file content."""
        key_file = tmp_path / "api_key.txt"
        key_file.write_text("  key-with-spaces  \n")
        result = resolve_api_key(f"{{file:{key_file}}}")
        assert isinstance(result, Success)
        assert result.unwrap() == "key-with-spaces"

    def test_resolve_api_key_file_missing_returns_failure(self):
        """{file:/path} with missing file returns Failure."""
        result = resolve_api_key("{file:/nonexistent/path/key.txt}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_file_nested_path(self, tmp_path):
        """{file:/path} works with nested directory paths."""
        nested = tmp_path / "secrets" / "keys"
        nested.mkdir(parents=True)
        key_file = nested / "openai.txt"
        key_file.write_text("nested-key")
        result = resolve_api_key(f"{{file:{key_file}}}")
        assert isinstance(result, Success)
        assert result.unwrap() == "nested-key"

    def test_resolve_api_key_file_home_expansion(self, monkeypatch, tmp_path):
        """{file:/path} correctly expands ~ to home directory.

        This test verifies that paths starting with ~ are properly expanded
        and that the expanded path is used for file operations.
        """
        # Set up a fake home directory
        fake_home = tmp_path / "fake_home"
        fake_home.mkdir()
        key_file = fake_home / "api_key.txt"
        key_file.write_text("home-expanded-key")

        # Monkeypatch HOME environment variable for expanduser()
        monkeypatch.setenv("HOME", str(fake_home))

        # Use ~ in the path - should be expanded to fake_home
        result = resolve_api_key("{file:~/api_key.txt}")
        assert isinstance(result, Success), f"Failed with: {result}"
        assert result.unwrap() == "home-expanded-key"

    def test_resolve_api_key_file_home_expansion_nested(self, monkeypatch, tmp_path):
        """{file:/path} expands ~ in nested paths."""
        fake_home = tmp_path / "fake_home"
        fake_home.mkdir()
        nested = fake_home / "secrets" / "api"
        nested.mkdir(parents=True)
        key_file = nested / "key.txt"
        key_file.write_text("nested-home-key")

        monkeypatch.setenv("HOME", str(fake_home))

        result = resolve_api_key("{file:~/secrets/api/key.txt}")
        assert isinstance(result, Success), f"Failed with: {result}"
        assert result.unwrap() == "nested-home-key"


class TestResolveApiKeyAuth:
    """Test {auth:provider} variable resolution."""

    def test_resolve_api_key_auth_success(self, monkeypatch, tmp_path):
        """{auth:provider} resolution returns auth key."""
        auth_file = tmp_path / "auth.json"
        auth_file.write_text('{"openai": {"api_key": "auth-openai-key"}}')
        monkeypatch.setattr("lattice.shell.auth.AUTH_FILE", auth_file)

        result = resolve_api_key("{auth:openai}")
        assert isinstance(result, Success)
        assert result.unwrap() == "auth-openai-key"

    def test_resolve_api_key_auth_missing_provider_returns_failure(
        self, monkeypatch, tmp_path
    ):
        """{auth:provider} with missing provider returns Failure."""
        auth_file = tmp_path / "auth.json"
        auth_file.write_text('{"openai": {"api_key": "key"}}')
        monkeypatch.setattr("lattice.shell.auth.AUTH_FILE", auth_file)

        result = resolve_api_key("{auth:anthropic}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_auth_missing_file_returns_failure(
        self, monkeypatch, tmp_path
    ):
        """{auth:provider} with missing auth file returns Failure."""
        # Point to a non-existent file
        auth_file = tmp_path / "nonexistent" / "auth.json"
        monkeypatch.setattr("lattice.shell.auth.AUTH_FILE", auth_file)

        result = resolve_api_key("{auth:openai}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_auth_multiple_providers(self, monkeypatch, tmp_path):
        """{auth:provider} works with multiple providers in auth file."""
        auth_file = tmp_path / "auth.json"
        auth_file.write_text(
            '{"openai": {"api_key": "sk-openai"}, "anthropic": {"api_key": "sk-anthropic"}}'
        )
        monkeypatch.setattr("lattice.shell.auth.AUTH_FILE", auth_file)

        result_openai = resolve_api_key("{auth:openai}")
        result_anthropic = resolve_api_key("{auth:anthropic}")

        assert isinstance(result_openai, Success)
        assert result_openai.unwrap() == "sk-openai"
        assert isinstance(result_anthropic, Success)
        assert result_anthropic.unwrap() == "sk-anthropic"


class TestResolveApiKeyInvalidSyntax:
    """Test invalid variable syntax handling."""

    def test_resolve_api_key_invalid_syntax_prefix(self):
        """Invalid variable prefix returns Failure."""
        result = resolve_api_key("{invalid:something}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_malformed_braces_passthrough(self):
        """Malformed braces (no closing brace) are treated as literal string."""
        # {env:VAR (missing closing brace) doesn't match variable pattern
        # so it's passed through as a literal string
        result = resolve_api_key("{env:VAR")
        assert isinstance(result, Success)
        assert result.unwrap() == "{env:VAR"

    def test_resolve_api_key_missing_colon(self):
        """Missing colon in variable syntax returns Failure."""
        result = resolve_api_key("{envVAR}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_empty_variable_content(self):
        """Empty variable content returns Failure."""
        result = resolve_api_key("{env:}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_file_empty_path(self):
        """Empty file path returns Failure."""
        result = resolve_api_key("{file:}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_auth_empty_provider(self):
        """Empty provider name returns Failure."""
        result = resolve_api_key("{auth:}")
        assert isinstance(result, Failure)


class TestResolveApiKeyFileSecurityPathTraversal:
    """Test path traversal protection in {file:/path} resolution."""

    def test_resolve_api_key_file_blocks_parent_directory_traversal(self, tmp_path):
        """{file:/path} blocks .. path traversal."""
        # Create a file in a subdirectory
        subdir = tmp_path / "secrets"
        subdir.mkdir()
        key_file = subdir / "key.txt"
        key_file.write_text("secret-key")

        # Try to access via traversal (simulated - Path doesn't normalize in constructor)
        # The issue is paths like "../../../etc/passwd" being passed directly
        result = resolve_api_key("{file:../etc/passwd}")
        assert isinstance(result, Failure)
        assert "traversal" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_double_dot_in_path(self):
        """{file:/path} blocks any path containing .."""
        result = resolve_api_key("{file:/some/../etc/passwd}")
        assert isinstance(result, Failure)
        assert "traversal" in str(result.failure()).lower()

    def test_resolve_api_key_file_allows_double_dot_in_filename(self, tmp_path):
        """{file:/path} allows benign filenames containing '..' as substring.

        Path traversal detection is path-part-aware. A filename like
        'file..backup.txt' is allowed because '..' is not a path component.
        """
        key_file = tmp_path / "config..backup.txt"
        key_file.write_text("backup-key")
        result = resolve_api_key(f"{{file:{key_file}}}")
        assert isinstance(result, Success), f"Should allow '..' in filename: {key_file}"
        assert result.unwrap() == "backup-key"

    def test_resolve_api_key_file_allows_double_dot_in_nested_filename(self, tmp_path):
        """{file:/path} allows '..' in filename within nested directories."""
        nested = tmp_path / "configs" / "backups"
        nested.mkdir(parents=True)
        key_file = nested / "api..key..backup.txt"
        key_file.write_text("nested-backup-key")
        result = resolve_api_key(f"{{file:{key_file}}}")
        assert isinstance(result, Success), "Should allow '..' in nested filename"
        assert result.unwrap() == "nested-backup-key"

    def test_resolve_api_key_file_blocks_path_with_dotdot_component(self):
        """{file:/path} blocks paths with '..' as a path component."""
        result = resolve_api_key("{file:safe/../other}")
        assert isinstance(result, Failure)
        assert "traversal" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_absolute_dotdot_traversal(self):
        """{file:/path} blocks absolute paths with '..' component."""
        result = resolve_api_key("{file:/home/user/../root/.ssh/id_rsa}")
        assert isinstance(result, Failure)
        assert "traversal" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_etc_passwd(self):
        """{file:/path} blocks access to /etc/passwd."""
        result = resolve_api_key("{file:/etc/passwd}")
        assert isinstance(result, Failure)
        assert "sensitive" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_etc_shadow(self):
        """{file:/path} blocks access to /etc/shadow."""
        result = resolve_api_key("{file:/etc/shadow}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_file_blocks_proc_directory(self):
        """{file:/path} blocks access to /proc/."""
        result = resolve_api_key("{file:/proc/self/environ}")
        assert isinstance(result, Failure)
        assert "sensitive" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_sys_directory(self):
        """{file:/path} blocks access to /sys/."""
        result = resolve_api_key("{file:/sys/kernel/security}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_file_blocks_root_directory(self, monkeypatch):
        """{file:/path} blocks access to /root/."""
        result = resolve_api_key("{file:/root/.bashrc}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_file_blocks_ssh_directory(self, monkeypatch, tmp_path):
        """{file:/path} blocks access to ~/.ssh/."""
        # Create a fake .ssh directory
        fake_home = tmp_path / "home"
        fake_home.mkdir()
        ssh_dir = fake_home / ".ssh"
        ssh_dir.mkdir()
        ssh_key = ssh_dir / "id_rsa"
        ssh_key.write_text("PRIVATE KEY")

        # Monkeypatch home to tmp_path
        monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)

        result = resolve_api_key(f"{{file:{ssh_key}}}")
        assert isinstance(result, Failure)
        assert "sensitive" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_ssh_directory_exact(
        self, monkeypatch, tmp_path
    ):
        """{file:/path} blocks access to ~/.ssh directory itself (no trailing slash).

        This test verifies the fix for blocking exact directory paths,
        not just files within the directory.
        """
        # Create a fake .ssh directory with a file inside
        fake_home = tmp_path / "home"
        fake_home.mkdir()
        ssh_dir = fake_home / ".ssh"
        ssh_dir.mkdir()
        # Create a file that we'll try to read from the exact .ssh path
        # Note: can't read a directory, but validation should fail before read attempt
        ssh_key = ssh_dir / "id_rsa"
        ssh_key.write_text("PRIVATE KEY")

        monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)

        # Try to access ~/.ssh itself - should be blocked
        result = resolve_api_key(f"{{file:{ssh_dir}}}")
        assert isinstance(result, Failure), "Should block exact .ssh directory"
        assert "sensitive" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_gnupg_directory_exact(
        self, monkeypatch, tmp_path
    ):
        """{file:/path} blocks access to ~/.gnupg directory itself."""
        fake_home = tmp_path / "home"
        fake_home.mkdir()
        gnupg_dir = fake_home / ".gnupg"
        gnupg_dir.mkdir()

        monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)

        result = resolve_api_key(f"{{file:{gnupg_dir}}}")
        assert isinstance(result, Failure), "Should block exact .gnupg directory"
        assert "sensitive" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_pgp_directory_exact(
        self, monkeypatch, tmp_path
    ):
        """{file:/path} blocks access to ~/.pgp directory itself."""
        fake_home = tmp_path / "home"
        fake_home.mkdir()
        pgp_dir = fake_home / ".pgp"
        pgp_dir.mkdir()

        monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)

        result = resolve_api_key(f"{{file:{pgp_dir}}}")
        assert isinstance(result, Failure), "Should block exact .pgp directory"
        assert "sensitive" in str(result.failure()).lower()

    def test_resolve_api_key_file_blocks_gnupg_directory(self, monkeypatch, tmp_path):
        """{file:/path} blocks access to ~/.gnupg/."""
        fake_home = tmp_path / "home"
        fake_home.mkdir()
        gnupg_dir = fake_home / ".gnupg"
        gnupg_dir.mkdir()
        gpg_key = gnupg_dir / "private.key"
        gpg_key.write_text("GPG PRIVATE KEY")

        monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)

        result = resolve_api_key(f"{{file:{gpg_key}}}")
        assert isinstance(result, Failure)

    def test_resolve_api_key_file_allows_safe_path(self, tmp_path):
        """{file:/path} allows access to safe, non-sensitive paths."""
        safe_file = tmp_path / "api_key.txt"
        safe_file.write_text("my-safe-key")
        result = resolve_api_key(f"{{file:{safe_file}}}")
        assert isinstance(result, Success)
        assert result.unwrap() == "my-safe-key"

    def test_resolve_api_key_file_allows_project_directory(self, tmp_path):
        """{file:/path} allows access to project directory files."""
        project_config = tmp_path / ".lattice" / "config"
        project_config.parent.mkdir()
        project_config.write_text("project-key")
        result = resolve_api_key(f"{{file:{project_config}}}")
        assert isinstance(result, Success)


class TestResolveApiKeyFileSymlinkSecurity:
    """Test symlink-based path traversal protection in {file:/path} resolution.

    Policy: Option B - Allow symlinks but validate resolved target.
    This blocks malicious symlinks pointing to sensitive paths while
    permitting legitimate symlinks within safe directories.
    """

    def test_resolve_api_key_file_blocks_symlink_to_etc_passwd(self, tmp_path):
        """{file:/path} blocks symlink pointing to /etc/passwd."""
        # Create a symlink in safe directory pointing to sensitive file
        symlink = tmp_path / "api_key.txt"
        # Note: /etc/passwd must exist for true symlink test; we test the validation
        # by creating a symlink that resolves to /etc/passwd
        try:
            symlink.symlink_to("/etc/passwd")
            result = resolve_api_key(f"{{file:{symlink}}}")
            assert isinstance(result, Failure), "Should block symlink to /etc/passwd"
            assert "sensitive" in str(result.failure()).lower()
        except OSError:
            # Symlink creation may fail on some systems; skip test
            pytest.skip("Cannot create symlink on this system")

    def test_resolve_api_key_file_blocks_symlink_to_proc_self(self, tmp_path):
        """{file:/path} blocks symlink pointing to /proc/self."""
        symlink = tmp_path / "env.txt"
        try:
            symlink.symlink_to("/proc/self/environ")
            result = resolve_api_key(f"{{file:{symlink}}}")
            assert isinstance(result, Failure), "Should block symlink to /proc"
            assert "sensitive" in str(result.failure()).lower()
        except OSError:
            pytest.skip("Cannot create symlink on this system")

    def test_resolve_api_key_file_blocks_symlink_chain_to_sensitive(self, tmp_path):
        """{file:/path} blocks multi-level symlink chains to sensitive paths."""
        # Create chain: link1 -> link2 -> /etc/passwd
        link2 = tmp_path / "link2"
        link1 = tmp_path / "link1"
        try:
            link2.symlink_to("/etc/passwd")
            link1.symlink_to(link2)
            result = resolve_api_key(f"{{file:{link1}}}")
            assert isinstance(result, Failure), (
                "Should block symlink chain to sensitive path"
            )
        except OSError:
            pytest.skip("Cannot create symlink on this system")

    def test_resolve_api_key_file_blocks_broken_symlink_in_safe_dir(self, tmp_path):
        """{file:/path} fails gracefully for broken symlinks."""
        broken_link = tmp_path / "broken.txt"
        try:
            broken_link.symlink_to("/nonexistent/path/that/does/not/exist")
            result = resolve_api_key(f"{{file:{broken_link}}}")
            # File reading will fail since target doesn't exist
            assert isinstance(result, Failure)
        except OSError:
            pytest.skip("Cannot create symlink on this system")

    def test_resolve_api_key_file_blocks_symlink_to_ssh_dir(self, tmp_path):
        """{file:/path} blocks symlink pointing to ~/.ssh/."""
        ssh_link = tmp_path / "key.txt"
        # Point to a hypothetical .ssh path
        ssh_target = tmp_path / "fake_home" / ".ssh" / "id_rsa"
        ssh_target.parent.mkdir(parents=True)
        ssh_target.write_text("PRIVATE KEY")
        try:
            ssh_link.symlink_to(ssh_target)
            result = resolve_api_key(f"{{file:{ssh_link}}}")
            assert isinstance(result, Failure), "Should block symlink to .ssh"
            assert "sensitive" in str(result.failure()).lower()
        except OSError:
            pytest.skip("Cannot create symlink on this system")

    def test_resolve_api_key_file_allows_legitimate_symlink(self, tmp_path):
        """{file:/path} allows symlink within safe directories."""
        # Create a real file and symlink to it
        real_file = tmp_path / "real_key.txt"
        real_file.write_text("actual-api-key")
        symlink = tmp_path / "key_link.txt"
        try:
            symlink.symlink_to(real_file)
            result = resolve_api_key(f"{{file:{symlink}}}")
            assert isinstance(result, Success), "Should allow safe symlink"
            assert result.unwrap() == "actual-api-key"
        except OSError:
            pytest.skip("Cannot create symlink on this system")

    def test_resolve_api_key_file_symlink_to_root_directory(self, tmp_path):
        """{file:/path} blocks symlink pointing to /root/."""
        root_link = tmp_path / "root_file.txt"
        try:
            root_link.symlink_to("/root/.bashrc")
            result = resolve_api_key(f"{{file:{root_link}}}")
            assert isinstance(result, Failure), "Should block symlink to /root"
            assert "sensitive" in str(result.failure()).lower()
        except OSError:
            pytest.skip("Cannot create symlink on this system")
