"""Tests for CLI commands."""

import tempfile
from pathlib import Path

import pytest
from click.testing import CliRunner

from scry_run.cli import main
from scry_run.cli.init import init
from scry_run.cli.cache import cache
from scry_run.cache import ScryCache


@pytest.fixture
def runner():
    """Create a Click test runner."""
    return CliRunner()


@pytest.fixture
def temp_dir():
    """Create a temporary directory."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


class TestMainCLI:
    """Tests for main CLI entry point."""

    def test_help(self, runner):
        """Test --help option."""
        result = runner.invoke(main, ["--help"])
        
        assert result.exit_code == 0
        assert "scry-run" in result.output
        assert "init" in result.output
        assert "cache" in result.output

    def test_version(self, runner):
        """Test --version option."""
        result = runner.invoke(main, ["--version"])
        
        assert result.exit_code == 0
        assert "0.1.0" in result.output or "version" in result.output.lower()


class TestInitCommand:
    """Tests for init command with centralized home."""

    def test_init_help(self, runner):
        """Test init --help."""
        result = runner.invoke(init, ["--help"])

        assert result.exit_code == 0
        assert "--name" in result.output
        assert "--description" in result.output

    def test_init_creates_app_in_home(self, scry_run_home):
        """Test init creates app in ~/.scry-run/apps/<name>/."""
        runner = CliRunner()
        result = runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "Test application", "--no-auto-expand"],
        )

        assert result.exit_code == 0
        app_dir = scry_run_home / "apps" / "test-app"
        assert app_dir.exists()
        assert (app_dir / "app.py").exists()
        assert (app_dir / "cache.json").exists()
        assert (app_dir / "logs").exists()

    def test_init_app_py_is_executable(self, scry_run_home):
        """Test app.py has shebang and is executable."""
        runner = CliRunner()
        runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "Test", "--no-auto-expand"],
        )

        app_py = scry_run_home / "apps" / "test-app" / "app.py"
        content = app_py.read_text()
        assert content.startswith("#!/usr/bin/env python3")

        # Check executable bit
        import stat
        mode = app_py.stat().st_mode
        assert mode & stat.S_IXUSR  # User execute bit

    def test_init_creates_empty_cache(self, scry_run_home):
        """Test init creates empty cache.json."""
        runner = CliRunner()
        runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "Test", "--no-auto-expand"],
        )

        cache_file = scry_run_home / "apps" / "test-app" / "cache.json"
        import json
        data = json.loads(cache_file.read_text())
        assert data == {}

    def test_init_refuses_duplicate_name(self, scry_run_home):
        """Test init refuses to overwrite existing app without confirm."""
        runner = CliRunner()

        # Create first app
        runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "First", "--no-auto-expand"],
        )

        # Try to create again
        result = runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "Second", "--no-auto-expand"],
            input="n\n",  # Don't overwrite
        )

        assert "already exists" in result.output.lower() or result.exit_code != 0

    def test_init_converts_name_to_class(self, scry_run_home):
        """Test that init converts name to valid class name."""
        runner = CliRunner()
        result = runner.invoke(
            main,
            ["init", "--name", "my-cool-app", "--description", "Test", "--no-auto-expand"],
        )

        assert result.exit_code == 0

        app_file = scry_run_home / "apps" / "my-cool-app" / "app.py"
        assert app_file.exists()

        content = app_file.read_text()
        assert "class MyCoolApp(ScryClass):" in content

    def test_init_invalid_name(self, scry_run_home):
        """Test that init rejects invalid names."""
        runner = CliRunner()
        result = runner.invoke(
            main,
            ["init", "--name", "my@invalid!name", "--description", "Test", "--no-auto-expand"],
        )

        assert result.exit_code != 0
        assert "alphanumeric" in result.output.lower()

    def test_init_overwrite_accept(self, scry_run_home):
        """Test that init overwrites when confirmed."""
        runner = CliRunner()

        # Create first app
        runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "First", "--no-auto-expand"],
        )

        # Overwrite with confirmation
        result = runner.invoke(
            main,
            ["init", "--name", "test-app", "--description", "Second", "--no-auto-expand"],
            input="y\n",
        )

        assert result.exit_code == 0
        app_file = scry_run_home / "apps" / "test-app" / "app.py"
        content = app_file.read_text()
        assert "Second" in content


class TestCacheCommands:
    """Tests for cache commands with centralized home."""

    def test_cache_help(self, runner):
        """Test cache --help."""
        result = runner.invoke(cache, ["--help"])

        assert result.exit_code == 0
        assert "list" in result.output
        assert "export" in result.output
        assert "prune" in result.output

    def test_cache_list_shows_apps(self, scry_run_home):
        """Test cache list shows all apps."""
        runner = CliRunner()

        # Create two apps
        runner.invoke(main, ["init", "--name", "app1", "--description", "First", "--no-auto-expand"])
        runner.invoke(main, ["init", "--name", "app2", "--description", "Second", "--no-auto-expand"])

        result = runner.invoke(main, ["cache", "list"])

        assert result.exit_code == 0
        assert "app1" in result.output
        assert "app2" in result.output

    def test_cache_list_no_apps(self, scry_run_home):
        """Test cache list with no apps."""
        runner = CliRunner()

        result = runner.invoke(main, ["cache", "list"])

        assert result.exit_code == 0
        assert "No apps found" in result.output

    def test_cache_list_specific_app(self, scry_run_home):
        """Test cache list for specific app."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        result = runner.invoke(main, ["cache", "list", "myapp"])

        assert result.exit_code == 0
        # Empty cache should show a message
        assert "No cached entries" in result.output or result.exit_code == 0

    def test_cache_list_specific_app_with_entries(self, scry_run_home):
        """Test cache list for specific app with cached entries."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add some cache entries directly
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method1", "def method1(): pass", docstring="Test")
        llm_cache.set("MyClass", "method2", "def method2(): pass", docstring="Test 2")

        result = runner.invoke(main, ["cache", "list", "myapp"])

        assert result.exit_code == 0
        assert "MyClass" in result.output
        assert "method1" in result.output
        assert "method2" in result.output
        assert "Total: 2 entries" in result.output

    def test_cache_list_nonexistent_app(self, scry_run_home):
        """Test cache list for nonexistent app."""
        runner = CliRunner()

        result = runner.invoke(main, ["cache", "list", "nonexistent"])

        assert result.exit_code == 0
        assert "not found" in result.output.lower()

    def test_cache_show(self, scry_run_home):
        """Test cache show command."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entry
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set(
            "MyClass",
            "my_method",
            "def my_method(self):\n    return 42",
            docstring="Returns 42",
            dependencies=["import json"],
        )

        result = runner.invoke(main, ["cache", "show", "myapp", "MyClass.my_method"])

        assert result.exit_code == 0
        assert "MyClass" in result.output
        assert "my_method" in result.output
        assert "return 42" in result.output

    def test_cache_show_not_found(self, scry_run_home):
        """Test cache show with nonexistent entry."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        result = runner.invoke(main, ["cache", "show", "myapp", "NoClass.no_method"])

        assert result.exit_code != 0
        assert "not found" in result.output.lower()

    def test_cache_show_invalid_key_format(self, scry_run_home):
        """Test cache show with invalid key format."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        result = runner.invoke(main, ["cache", "show", "myapp", "invalid_key"])

        assert result.exit_code != 0
        assert "invalid" in result.output.lower()

    def test_cache_export_json(self, scry_run_home, temp_dir):
        """Test cache export to JSON."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entry
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method", "def method(): pass")

        output_file = temp_dir / "export.json"

        result = runner.invoke(main, [
            "cache", "export", "myapp",
            "--output", str(output_file),
            "--format", "json",
        ])

        assert result.exit_code == 0
        assert output_file.exists()

        import json
        data = json.loads(output_file.read_text())
        assert "MyClass" in data

    def test_cache_export_python(self, scry_run_home, temp_dir):
        """Test cache export to Python."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entry
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method", "def method(): pass")

        output_file = temp_dir / "export.py"

        result = runner.invoke(main, [
            "cache", "export", "myapp",
            "--output", str(output_file),
            "--format", "python",
        ])

        assert result.exit_code == 0
        assert output_file.exists()
        assert "def method():" in output_file.read_text()

    def test_cache_export_python_requires_output(self, scry_run_home):
        """Test that python export requires --output."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        result = runner.invoke(main, [
            "cache", "export", "myapp",
            "--format", "python",
        ])

        assert result.exit_code != 0
        assert "--output required" in result.output

    def test_cache_prune_specific(self, scry_run_home):
        """Test cache prune specific entry."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entries
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method1", "code1")
        llm_cache.set("MyClass", "method2", "code2")

        result = runner.invoke(main, [
            "cache", "prune", "myapp",
            "--class", "MyClass",
            "--attr", "method1",
            "--all",  # Skip confirmation
        ])

        assert result.exit_code == 0
        # Reload cache to check entries
        llm_cache = ScryCache(cache_file)
        assert llm_cache.get("MyClass", "method1") is None
        assert llm_cache.get("MyClass", "method2") is not None

    def test_cache_prune_class(self, scry_run_home):
        """Test cache prune entire class."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entries
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method1", "code1")
        llm_cache.set("MyClass", "method2", "code2")
        llm_cache.set("OtherClass", "method", "code3")

        result = runner.invoke(main, [
            "cache", "prune", "myapp",
            "--class", "MyClass",
            "--all",
        ])

        assert result.exit_code == 0
        assert "Pruned 2 entries" in result.output
        # Reload cache to check entries
        llm_cache = ScryCache(cache_file)
        assert llm_cache.get("OtherClass", "method") is not None

    def test_cache_prune_requires_class_for_attr(self, scry_run_home):
        """Test that --attr requires --class."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        result = runner.invoke(main, [
            "cache", "prune", "myapp",
            "--attr", "method",
        ])

        assert result.exit_code != 0
        assert "--class" in result.output

    def test_cache_rm_specific_entry(self, scry_run_home):
        """Test cache rm for specific entry."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entries
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method1", "code1")
        llm_cache.set("MyClass", "method2", "code2")

        result = runner.invoke(main, ["cache", "rm", "myapp", "MyClass.method1"])

        assert result.exit_code == 0
        assert "Removed" in result.output
        # Reload cache to check entries
        llm_cache = ScryCache(cache_file)
        assert llm_cache.get("MyClass", "method1") is None
        assert llm_cache.get("MyClass", "method2") is not None

    def test_cache_reset(self, scry_run_home):
        """Test cache reset command."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        # Add cache entries
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("MyClass", "method1", "code1")
        llm_cache.set("MyClass", "method2", "code2")

        result = runner.invoke(main, ["cache", "reset", "myapp", "--force"])

        assert result.exit_code == 0
        assert "reset complete" in result.output.lower()
        # Reload cache to check entries
        llm_cache = ScryCache(cache_file)
        assert len(llm_cache.list_entries()) == 0


class TestAppCommands:
    """Tests for app management commands (list, which, rm, reset)."""

    def test_list_no_apps(self, scry_run_home):
        """Test list with no apps."""
        runner = CliRunner()
        result = runner.invoke(main, ["list"])

        assert result.exit_code == 0
        assert "No apps found" in result.output

    def test_list_shows_apps(self, scry_run_home):
        """Test list shows created apps."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "app1", "--description", "First app", "--no-auto-expand"])
        runner.invoke(main, ["init", "--name", "app2", "--description", "Second app", "--no-auto-expand"])

        result = runner.invoke(main, ["list"])

        assert result.exit_code == 0
        assert "app1" in result.output
        assert "app2" in result.output
        assert "First app" in result.output
        assert "Second app" in result.output

    def test_which_existing_app(self, scry_run_home):
        """Test which returns path for existing app."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        result = runner.invoke(main, ["which", "myapp"])

        assert result.exit_code == 0
        assert "myapp" in result.output
        assert "app.py" in result.output

    def test_which_nonexistent_app(self, scry_run_home):
        """Test which fails for nonexistent app."""
        runner = CliRunner()

        result = runner.invoke(main, ["which", "nonexistent"])

        assert result.exit_code != 0
        assert "not found" in result.output.lower()

    def test_rm_existing_app(self, scry_run_home):
        """Test rm removes app."""
        runner = CliRunner()
        runner.invoke(main, ["init", "--name", "myapp", "--description", "Test", "--no-auto-expand"])

        app_dir = scry_run_home / "apps" / "myapp"
        assert app_dir.exists()

        result = runner.invoke(main, ["rm", "myapp", "--force"])

        assert result.exit_code == 0
        assert "Removed" in result.output
        assert not app_dir.exists()

    def test_rm_nonexistent_app(self, scry_run_home):
        """Test rm fails for nonexistent app."""
        runner = CliRunner()

        result = runner.invoke(main, ["rm", "nonexistent", "--force"])

        assert result.exit_code != 0
        assert "not found" in result.output.lower()

    def test_reset_preserves_description(self, scry_run_home):
        """Test reset preserves app name and description."""
        runner = CliRunner()
        original_desc = "My special app description"
        runner.invoke(main, ["init", "--name", "myapp", "--description", original_desc, "--no-auto-expand"])

        # Modify app.py to simulate generated code
        app_file = scry_run_home / "apps" / "myapp" / "app.py"
        original_content = app_file.read_text()
        app_file.write_text(original_content + "\n# Extra generated code\n")

        # Add cache entries
        cache_file = scry_run_home / "apps" / "myapp" / "cache.json"
        llm_cache = ScryCache(cache_file)
        llm_cache.set("Myapp", "method", "def method(self): return 42")

        result = runner.invoke(main, ["reset", "myapp", "--force"])

        assert result.exit_code == 0
        assert "Reset" in result.output
        assert original_desc in result.output

        # Verify app was reset
        new_content = app_file.read_text()
        assert "Extra generated code" not in new_content
        assert original_desc in new_content

        # Verify cache was cleared
        llm_cache = ScryCache(cache_file)
        assert len(llm_cache.list_entries()) == 0

    def test_reset_nonexistent_app(self, scry_run_home):
        """Test reset fails for nonexistent app."""
        runner = CliRunner()

        result = runner.invoke(main, ["reset", "nonexistent", "--force"])

        assert result.exit_code != 0
        assert "not found" in result.output.lower()
