"""
Tests for CLI commands.

This module provides comprehensive tests for the ToRivers SDK CLI,
testing each command's functionality and error handling.
"""

from __future__ import annotations

import json
import os
import shutil
import tempfile
from pathlib import Path
from unittest.mock import patch

import httpx
import pytest
from click.testing import CliRunner

from torivers_sdk.cli.main import cli

# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture(autouse=True)
def restore_cwd():
    """Restore working directory after each test."""
    original_cwd = os.getcwd()
    yield
    try:
        os.chdir(original_cwd)
    except FileNotFoundError:
        # If the original dir was deleted, go to a safe location
        os.chdir(tempfile.gettempdir())


@pytest.fixture
def runner() -> CliRunner:
    """Create a CLI test runner."""
    return CliRunner()


@pytest.fixture
def temp_dir():
    """Create a temporary directory for tests."""
    temp = tempfile.mkdtemp()
    yield Path(temp)
    # Go back to a safe dir before cleanup
    try:
        os.chdir(tempfile.gettempdir())
    except FileNotFoundError:
        pass
    shutil.rmtree(temp, ignore_errors=True)


@pytest.fixture
def automation_project(temp_dir: Path):
    """Create a mock automation project."""
    # Create automation.yaml
    manifest = {
        "name": "test-automation",
        "version": "1.0.0",
        "description": "A test automation",
        "input_schema": {"type": "object"},
        "output_schema": {"type": "object"},
    }
    (temp_dir / "automation.yaml").write_text(
        "\n".join(
            f"{k}: {json.dumps(v) if isinstance(v, dict) else v}"
            for k, v in manifest.items()
        )
    )

    # Create main.py
    (temp_dir / "main.py").write_text(
        '''
"""Test automation."""
from langgraph.graph import StateGraph, START, END
from torivers_sdk.automation import Automation, AutomationMetadata
from typing_extensions import TypedDict


class TestState(TypedDict, total=False):
    input_data: dict
    output_data: dict


class TestAutomation(Automation[TestState]):
    @property
    def metadata(self) -> AutomationMetadata:
        return AutomationMetadata(
            name="test-automation",
            version="1.0.0",
            description="A test automation",
        )

    def get_state_class(self):
        return TestState

    def build_graph(self, builder):
        builder.add_node("process", lambda s: {
            "output_data": {
                "_version": 1,
                "_blocks": [{
                    "type": "text",
                    "label": "Result",
                    "content": "ok",
                    "format": "plain",
                }],
            }
        })
        builder.add_edge(START, "process")
        builder.add_edge("process", END)
        return builder


automation = TestAutomation()
'''
    )

    # Create tests directory
    tests_dir = temp_dir / "tests"
    tests_dir.mkdir()
    (tests_dir / "__init__.py").write_text("")
    (tests_dir / "test_automation.py").write_text(
        """
def test_dummy():
    assert True
"""
    )

    # Create requirements.txt
    (temp_dir / "requirements.txt").write_text("torivers-sdk>=0.1.0b5\n")

    # Create README.md
    (temp_dir / "README.md").write_text("# Test Automation\n")

    # Create sample_input.json
    (temp_dir / "sample_input.json").write_text('{"data": "test"}\n')

    return temp_dir


# =============================================================================
# CLI Group Tests
# =============================================================================


class TestCLIGroup:
    """Test suite for CLI group commands."""

    def test_cli_help(self, runner: CliRunner) -> None:
        """Test CLI help output."""
        result = runner.invoke(cli, ["--help"])
        assert result.exit_code == 0
        assert "ToRivers SDK" in result.output
        assert "init" in result.output
        assert "run" in result.output
        assert "test" in result.output
        assert "validate" in result.output
        assert "submit" in result.output

    def test_cli_version(self, runner: CliRunner) -> None:
        """Test CLI version output."""
        result = runner.invoke(cli, ["--version"])
        assert result.exit_code == 0
        assert "torivers-sdk" in result.output


# =============================================================================
# Init Command Tests
# =============================================================================


class TestInitCommand:
    """Test suite for init command."""

    def test_init_basic_project(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test initializing a basic project."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["init", "my-automation"])
            assert result.exit_code == 0
            assert "Creating project" in result.output
            assert "Success" in result.output

            # Check project was created
            project_dir = Path("my-automation")
            assert project_dir.exists()
            assert (project_dir / "automation.yaml").exists()
            assert (project_dir / "main.py").exists()
            assert (project_dir / "state.py").exists()
            assert (project_dir / ".gitignore").exists()

    def test_init_with_template(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test initializing with a specific template."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(
                cli, ["init", "data-project", "--template", "data-pipeline"]
            )
            assert result.exit_code == 0
            assert "Creating project" in result.output

    def test_init_invalid_name(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test initializing with invalid project name."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["init", "123-invalid"])
            assert result.exit_code == 1
            assert "Error" in result.output

    def test_init_existing_directory(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test initializing when directory already exists."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            Path("existing").mkdir()
            result = runner.invoke(cli, ["init", "existing"])
            assert result.exit_code == 1
            assert "already exists" in result.output

    def test_templates_command(self, runner: CliRunner) -> None:
        """Test listing templates."""
        result = runner.invoke(cli, ["templates"])
        assert result.exit_code == 0
        assert "basic" in result.output
        assert "data-pipeline" in result.output
        assert "ai-analysis" in result.output
        assert "webhook-handler" in result.output


# =============================================================================
# Validate Command Tests
# =============================================================================


class TestValidateCommand:
    """Test suite for validate command."""

    def test_validate_not_in_project(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test validate when not in a project directory."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["validate"])
            assert result.exit_code == 1
            assert "Not in an automation project" in result.output

    def test_validate_valid_project(
        self, runner: CliRunner, automation_project: Path
    ) -> None:
        """Test validate with a valid project."""
        os.chdir(automation_project)
        result = runner.invoke(cli, ["validate"])
        # Should pass or have warnings only
        assert "Validating Automation" in result.output

    def test_validate_missing_manifest(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test validate when manifest is missing."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            # Create only main.py
            Path("main.py").write_text("# Empty file")
            result = runner.invoke(cli, ["validate"])
            assert (
                "Manifest file not found" in result.output or "Error" in result.output
            )

    def test_validate_strict_mode(
        self, runner: CliRunner, automation_project: Path
    ) -> None:
        """Test validate with strict mode."""
        os.chdir(automation_project)
        result = runner.invoke(cli, ["validate", "--strict"])
        assert "Validating Automation" in result.output


# =============================================================================
# Run Command Tests
# =============================================================================


class TestRunCommand:
    """Test suite for run command."""

    def test_run_not_in_project(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test run when not in a project directory."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["run"])
            assert result.exit_code == 1
            assert "Not in an automation project" in result.output

    def test_run_with_input_file(
        self, runner: CliRunner, automation_project: Path
    ) -> None:
        """Test run with input file."""
        os.chdir(automation_project)
        result = runner.invoke(cli, ["run", "--input", "sample_input.json"])
        assert (
            "Running automation" in result.output
            or "Running Automation" in result.output
        )


# =============================================================================
# Test Command Tests
# =============================================================================


class TestTestCommand:
    """Test suite for test command."""

    def test_test_not_in_project(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test test command when not in a project directory."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["test"])
            assert result.exit_code == 1


# =============================================================================
# Submit Command Tests
# =============================================================================


class TestSubmitCommand:
    """Test suite for submit command."""

    def test_submit_not_in_project(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test submit when not in a project directory."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["submit"])
            assert result.exit_code == 1
            assert "Not in an automation project" in result.output

    def test_submit_dry_run(self, runner: CliRunner, automation_project: Path) -> None:
        """Test submit with dry-run flag."""
        os.chdir(automation_project)
        result = runner.invoke(cli, ["submit", "--dry-run"])
        assert "Dry Run" in result.output or "Validating" in result.output


# =============================================================================
# Auth Command Tests
# =============================================================================


class TestAuthCommands:
    """Test suite for authentication commands."""

    def test_whoami_not_authenticated(self, runner: CliRunner) -> None:
        """Test whoami when not authenticated."""
        with patch.dict(os.environ, {"HOME": "/tmp/test_home"}):
            result = runner.invoke(cli, ["whoami"])
            assert (
                "Not authenticated" in result.output
                or "Authentication" in result.output
            )

    def test_logout_not_logged_in(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test logout when not logged in."""
        # Create temp home directory
        home_dir = temp_dir / "home"
        home_dir.mkdir()

        with patch.dict(os.environ, {"HOME": str(home_dir)}):
            result = runner.invoke(cli, ["logout"])
            assert result.exit_code == 0

    def test_login_stores_config_file(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test login stores validated token and user info in config."""
        home_dir = temp_dir / "home"
        home_dir.mkdir()

        with patch.dict(os.environ, {"HOME": str(home_dir)}):
            with patch("torivers_sdk.cli.commands.auth.webbrowser.open"):
                with patch(
                    "torivers_sdk.cli.commands.auth.Prompt.ask",
                    return_value="valid-token-123",
                ):
                    with patch("torivers_sdk.cli.commands.auth.httpx.Client") as client:
                        http = client.return_value.__enter__.return_value
                        http.get.return_value = httpx.Response(
                            status_code=200,
                            json={
                                "id": "dev_123",
                                "email": "dev@example.com",
                                "full_name": "Dev User",
                                "is_developer": True,
                            },
                            request=httpx.Request(
                                "GET", "https://app.torivers.com/api/developer/me"
                            ),
                        )

                        result = runner.invoke(cli, ["login"])
                        assert result.exit_code == 0

        config_path = home_dir / ".torivers" / "config.json"
        assert config_path.exists()
        config = json.loads(config_path.read_text())
        assert config["auth_token"] == "valid-token-123"
        assert config["user"]["email"] == "dev@example.com"
        assert config["user"]["developer_id"] == "dev_123"

    def test_login_rejects_invalid_token(
        self, runner: CliRunner, temp_dir: Path
    ) -> None:
        """Test login fails and does not persist config for invalid token."""
        home_dir = temp_dir / "home"
        home_dir.mkdir()

        with patch.dict(os.environ, {"HOME": str(home_dir)}):
            with patch("torivers_sdk.cli.commands.auth.webbrowser.open"):
                with patch(
                    "torivers_sdk.cli.commands.auth.Prompt.ask",
                    return_value="bad-token",
                ):
                    with patch("torivers_sdk.cli.commands.auth.httpx.Client") as client:
                        http = client.return_value.__enter__.return_value
                        http.get.return_value = httpx.Response(
                            status_code=401,
                            json={"error": "Unauthorized"},
                            request=httpx.Request(
                                "GET", "https://app.torivers.com/api/developer/me"
                            ),
                        )

                        result = runner.invoke(cli, ["login"])
                        assert result.exit_code == 1
                        assert "Unable to validate token" in result.output

        config_path = home_dir / ".torivers" / "config.json"
        assert not config_path.exists()


# =============================================================================
# Status Command Tests
# =============================================================================


class TestStatusCommand:
    """Test suite for status command."""

    def test_status_not_authenticated(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test status when not authenticated."""
        home_dir = temp_dir / "home"
        home_dir.mkdir()

        with patch.dict(os.environ, {"HOME": str(home_dir)}):
            result = runner.invoke(cli, ["status"])
            assert result.exit_code == 1
            assert "Not authenticated" in result.output

    def test_status_no_submissions(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test status when no submissions exist."""
        home_dir = temp_dir / "home"
        home_dir.mkdir()

        # Create config with auth token
        config_dir = home_dir / ".torivers"
        config_dir.mkdir()
        (config_dir / "config.json").write_text(
            json.dumps({"auth_token": "test-token"})
        )

        with patch.dict(os.environ, {"HOME": str(home_dir)}):
            with patch("torivers_sdk.cli.commands.status.httpx.Client") as client_cls:
                client = client_cls.return_value.__enter__.return_value
                client.get.return_value = httpx.Response(
                    status_code=200,
                    json={"submissions": []},
                    request=httpx.Request(
                        "GET",
                        "https://app.torivers.com/api/developer/submissions",
                    ),
                )

                result = runner.invoke(cli, ["status"])
                assert result.exit_code == 0
                assert "No submissions found" in result.output


# =============================================================================
# Init Templates Tests
# =============================================================================


class TestInitTemplates:
    """Test suite for project templates."""

    def test_basic_template_structure(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test basic template creates correct structure."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["init", "test-project"])
            assert result.exit_code == 0

            project = Path("test-project")
            assert (project / "automation.yaml").exists()
            assert (project / "main.py").exists()
            assert (project / "state.py").exists()
            assert (project / "nodes" / "__init__.py").exists()
            assert (project / "nodes" / "process.py").exists()
            assert (project / "tests" / "__init__.py").exists()
            assert (project / "tests" / "test_automation.py").exists()
            assert (project / ".gitignore").exists()
            assert (project / "requirements.txt").exists()
            assert (project / "README.md").exists()
            assert (project / "sample_input.json").exists()

            requirements = (project / "requirements.txt").read_text()
            assert "pytest>=8.0.0" in requirements
            assert requirements.count("pytest-asyncio>=") == 1

            manifest = (project / "automation.yaml").read_text()
            assert "estimated_duration_seconds:" in manifest
            assert "max_http_requests:" in manifest

            gitignore = (project / ".gitignore").read_text()
            assert ".venv/" in gitignore
            assert "__pycache__/" in gitignore
            assert ".pytest_cache/" in gitignore

            readme = (project / "README.md").read_text()
            assert "pip install -r requirements.txt" in readme
            assert "pip install -e ." not in readme

    def test_data_pipeline_template(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test data-pipeline template creates correct structure."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["init", "data-project", "-t", "data-pipeline"])
            assert result.exit_code == 0

            project = Path("data-project")
            assert (project / "nodes" / "fetch.py").exists()
            assert (project / "nodes" / "transform.py").exists()
            assert (project / "nodes" / "analyze.py").exists()

            requirements = (project / "requirements.txt").read_text()
            assert "pytest>=8.0.0" in requirements
            assert requirements.count("pytest-asyncio>=") == 1

            manifest = (project / "automation.yaml").read_text()
            assert "estimated_duration_seconds:" in manifest
            assert "max_http_requests:" in manifest

    def test_ai_analysis_template(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test ai-analysis template creates correct structure."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(cli, ["init", "ai-project", "-t", "ai-analysis"])
            assert result.exit_code == 0

            project = Path("ai-project")
            assert (project / "nodes" / "summarize.py").exists()
            assert (project / "nodes" / "analyze.py").exists()
            assert (project / "nodes" / "extract.py").exists()

            requirements = (project / "requirements.txt").read_text()
            assert "pytest>=8.0.0" in requirements
            assert requirements.count("pytest-asyncio>=") == 1

            manifest = (project / "automation.yaml").read_text()
            assert "estimated_duration_seconds:" in manifest
            assert "max_http_requests:" in manifest

    def test_webhook_handler_template(self, runner: CliRunner, temp_dir: Path) -> None:
        """Test webhook-handler template creates correct structure."""
        with runner.isolated_filesystem(temp_dir=str(temp_dir)):
            result = runner.invoke(
                cli, ["init", "webhook-project", "-t", "webhook-handler"]
            )
            assert result.exit_code == 0

            project = Path("webhook-project")
            assert (project / "nodes" / "validate.py").exists()
            assert (project / "nodes" / "process.py").exists()
            assert (project / "nodes" / "respond.py").exists()

            requirements = (project / "requirements.txt").read_text()
            assert "pytest>=8.0.0" in requirements
            assert requirements.count("pytest-asyncio>=") == 1

            manifest = (project / "automation.yaml").read_text()
            assert "estimated_duration_seconds:" in manifest
            assert "max_http_requests:" in manifest


# =============================================================================
# Validation Logic Tests
# =============================================================================


class TestValidationLogic:
    """Test suite for validation logic."""

    def test_version_validation(
        self, runner: CliRunner, automation_project: Path
    ) -> None:
        """Test version format validation."""
        # Write invalid version
        manifest_path = automation_project / "automation.yaml"
        manifest_path.write_text(
            """
name: test
version: invalid-version
description: test
"""
        )

        os.chdir(automation_project)
        result = runner.invoke(cli, ["validate"])
        assert (
            "Invalid version format" in result.output
            or "version" in result.output.lower()
        )


# =============================================================================
# Command Help Tests
# =============================================================================


class TestCommandHelp:
    """Test suite for command help messages."""

    @pytest.mark.parametrize(
        "command",
        ["init", "run", "test", "validate", "submit", "login", "logout", "status"],
    )
    def test_command_help(self, runner: CliRunner, command: str) -> None:
        """Test each command has help text."""
        result = runner.invoke(cli, [command, "--help"])
        assert result.exit_code == 0
        assert command in result.output or "Usage:" in result.output
