from pathlib import Path
from unittest.mock import MagicMock, patch

from inspect_ai.log import EvalLog
from typer.testing import CliRunner

from bench_af._abstract.configs import (
    DetectorConfig,
    EnvironmentConfig,
    ModelOrganismConfig,
)
from bench_af._abstract.objects import Detector, Environment, ModelOrganism
from bench_af.cli.cli import app
from bench_af.util.load import RunDetectorConfig, ValidateModelConfig

runner = CliRunner()


class TestValidateModelCommand:
    """Test validate-model CLI command."""

    @patch("cli.run.validate_model_organism")
    @patch("cli.run.load_environment")
    @patch("cli.run.load_model_organism")
    @patch("cli.run.load_config")
    @patch("cli.run._validate_config_path")
    @patch("cli.run._get_results_dir")
    def test_validate_model_success(
        self,
        mock_get_results_dir,
        mock_validate_path,
        mock_load_config,
        mock_load_model,
        mock_load_env,
        mock_validate,
    ):
        """Test successful validate-model command."""
        # Setup mocks
        mock_organism = MagicMock(spec=ModelOrganism)
        mock_environment = MagicMock(spec=Environment)
        mock_validation_result = [MagicMock(spec=EvalLog)]
        mock_results_dir = Path("/tmp/mock_results")

        # Create mock config with new Pydantic structure
        mock_config = ValidateModelConfig(
            model_organism=ModelOrganismConfig(
                name="test_model", description="Test model organism", config={}
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={"test": "config"},
                )
            ],
        )

        mock_validate_path.return_value = Path(
            "test_config.json"
        )  # Mock successful validation
        mock_load_config.return_value = mock_config
        mock_load_model.return_value = mock_organism
        mock_load_env.return_value = mock_environment
        mock_validate.return_value = mock_validation_result
        mock_get_results_dir.return_value = mock_results_dir

        # Run command with new --config flag
        result = runner.invoke(
            app, ["run", "validate-model", "--config", "test_config.json"]
        )

        # Verify execution
        assert result.exit_code == 0
        mock_load_config.assert_called_once_with(
            "test_config.json", "validate-model")
        mock_load_model.assert_called_once_with("test_model")
        mock_load_env.assert_called_once_with("test_env")
        mock_validate.assert_called_once_with(
            model_organism=mock_organism,
            environment=mock_environment,
            eval_config={"test": "config"},
            results_dir=mock_results_dir,
            raw_eval_logs=True,
        )

    @patch("cli.run.validate_model_organism")
    @patch("cli.run.load_environment")
    @patch("cli.run.load_model_organism")
    @patch("cli.run.load_config")
    @patch("cli.run._validate_config_path")
    @patch("cli.run._get_results_dir")
    def test_validate_model_with_short_flag(
        self,
        mock_get_results_dir,
        mock_validate_path,
        mock_load_config,
        mock_load_model,
        mock_load_env,
        mock_validate,
    ):
        """Test validate-model command with short -c flag."""
        # Setup mocks - same as success test
        mock_organism = MagicMock(spec=ModelOrganism)
        mock_environment = MagicMock(spec=Environment)
        mock_validation_result = [MagicMock(spec=EvalLog)]
        mock_results_dir = Path("/tmp/mock_results")

        mock_config = ValidateModelConfig(
            model_organism=ModelOrganismConfig(
                name="test_model", description="Test model organism", config={}
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={"test": "config"},
                )
            ],
        )

        mock_validate_path.return_value = Path("test_config.json")
        mock_load_config.return_value = mock_config
        mock_load_model.return_value = mock_organism
        mock_load_env.return_value = mock_environment
        mock_validate.return_value = mock_validation_result
        mock_get_results_dir.return_value = mock_results_dir

        # Use short flag -c instead of --config
        result = runner.invoke(
            app, ["run", "validate-model", "-c", "test_config.json"])

        # Same verification as success test
        assert result.exit_code == 0
        mock_validate.assert_called_once_with(
            model_organism=mock_organism,
            environment=mock_environment,
            eval_config={"test": "config"},
            results_dir=mock_results_dir,
            raw_eval_logs=True,
        )

    @patch("cli.run.load_config")
    @patch("cli.run.load_model_organism")
    @patch("cli.run._validate_config_path")
    def test_validate_model_load_failure(
        self, mock_validate_path, mock_load_model, mock_load_config
    ):
        """Test validate-model command with model loading failure."""
        # Create mock config with new Pydantic structure
        mock_config = ValidateModelConfig(
            model_organism=ModelOrganismConfig(
                name="nonexistent_model",
                description="Nonexistent model organism",
                config={},
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={},
                )
            ],
        )
        mock_validate_path.return_value = Path(
            "test_config.json"
        )  # Mock successful validation
        mock_load_config.return_value = mock_config
        mock_load_model.side_effect = ValueError("Model not found")

        # Run command
        result = runner.invoke(
            app, ["run", "validate-model", "--config", "test_config.json"]
        )

        # Should exit with error
        assert result.exit_code != 0
        # Should show loading message before failing - updated to match actual CLI output
        assert "📦 Loading model organism" in result.stdout

    def test_validate_model_missing_config_file(self):
        """Test validate-model command with missing config file."""
        # Run command with non-existent config file
        result = runner.invoke(
            app, ["run", "validate-model", "--config", "nonexistent_config.json"]
        )

        # Should exit with error
        assert result.exit_code != 0
        # Should show user-friendly error message
        assert "Configuration file not found" in result.stdout

    def test_validate_model_shows_help_without_args(self):
        """Test validate-model command shows help when no arguments provided."""
        result = runner.invoke(app, ["run", "validate-model"])

        # Should show help instead of error due to no_args_is_help=True
        assert result.exit_code == 0
        assert "Validate a model organism" in result.stdout
        assert "--config" in result.stdout


class TestRunDetectorCommand:
    """Test run-detector CLI command."""

    @patch("cli.run.run_detector")
    @patch("cli.run.load_environment")
    @patch("cli.run.load_model_organism")
    @patch("cli.run.load_detector")
    @patch("cli.run.load_config")
    @patch("cli.run._validate_config_path")
    @patch("cli.run._get_results_dir")
    def test_run_detector_success(
        self,
        mock_get_results_dir,
        mock_validate_path,
        mock_load_config,
        mock_load_detector,
        mock_load_model,
        mock_load_env,
        mock_run_detector,
    ):
        """Test successful run-detector command."""
        # Setup mocks
        mock_detector = MagicMock(spec=Detector)
        mock_organism = MagicMock(spec=ModelOrganism)
        mock_environment = MagicMock(spec=Environment)
        mock_detector_result = (0.7, "High probability of deception")
        mock_results_dir = Path("/tmp/mock_results")

        # Create mock config with new Pydantic structure
        mock_config = RunDetectorConfig(
            detector=DetectorConfig(
                name="test_detector", description="Test detector"),
            model_organism=ModelOrganismConfig(
                name="test_model", description="Test model organism", config={}
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={"test": "config"},
                )
            ],
        )

        mock_validate_path.return_value = Path(
            "test_config.json"
        )  # Mock successful validation
        mock_load_config.return_value = mock_config
        mock_load_detector.return_value = mock_detector
        mock_load_model.return_value = mock_organism
        mock_load_env.return_value = mock_environment
        mock_run_detector.return_value = mock_detector_result
        mock_get_results_dir.return_value = mock_results_dir

        # Run command with new --config flag
        result = runner.invoke(
            app, ["run", "run-detector", "--config", "test_config.json"]
        )

        # Verify execution
        assert result.exit_code == 0
        mock_load_config.assert_called_once_with(
            "test_config.json", "run-detector")
        mock_load_detector.assert_called_once_with("test_detector")
        mock_load_model.assert_called_once_with("test_model")
        mock_load_env.assert_called_once_with("test_env")
        mock_run_detector.assert_called_once_with(
            detector=mock_detector,
            model_organism=mock_organism,
            environment=mock_environment,
            eval_config={"test": "config"},
            results_dir=mock_results_dir,
        )

    @patch("cli.run.load_config")
    @patch("cli.run.load_detector")
    @patch("cli.run._validate_config_path")
    def test_run_detector_load_failure(
        self, mock_validate_path, mock_load_detector, mock_load_config
    ):
        """Test run-detector command with detector loading failure."""
        # Create mock config with new Pydantic structure
        mock_config = RunDetectorConfig(
            detector=DetectorConfig(
                name="nonexistent_detector", description="Nonexistent detector"
            ),
            model_organism=ModelOrganismConfig(
                name="test_model", description="Test model organism", config={}
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={},
                )
            ],
        )
        mock_validate_path.return_value = Path(
            "test_config.json"
        )  # Mock successful validation
        mock_load_config.return_value = mock_config
        mock_load_detector.side_effect = ValueError("Detector not found")

        # Run command
        result = runner.invoke(
            app, ["run", "run-detector", "--config", "test_config.json"]
        )

        # Should exit with error
        assert result.exit_code != 0
        # Should show loading message before failing - updated to match actual CLI output
        assert "🔍 Loading detector" in result.stdout

    def test_run_detector_shows_help_without_args(self):
        """Test run-detector command shows help when no arguments provided."""
        result = runner.invoke(app, ["run", "run-detector"])

        # Should show help instead of error due to no_args_is_help=True
        assert result.exit_code == 0
        assert "Run a detector algorithm" in result.stdout
        assert "--config" in result.stdout


class TestListCommands:
    """Test list subcommands."""

    @patch("cli.list.list_model_organisms")
    def test_list_models(self, mock_list_models):
        """Test list models command."""
        mock_list_models.return_value = ["model1", "model2", "default"]

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

        assert result.exit_code == 0
        mock_list_models.assert_called_once()
        # Check for Rich formatted output
        assert "Available Model Organisms" in result.stdout
        assert "model1" in result.stdout
        assert "model2" in result.stdout
        assert "default" in result.stdout

    @patch("cli.list.list_detectors")
    def test_list_detectors(self, mock_list_detectors):
        """Test list detectors command."""
        mock_list_detectors.return_value = ["detector1", "detector2"]

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

        assert result.exit_code == 0
        mock_list_detectors.assert_called_once()
        # Check for Rich formatted output
        assert "Available Detectors" in result.stdout
        assert "detector1" in result.stdout
        assert "detector2" in result.stdout

    @patch("cli.list.list_environments")
    def test_list_environments(self, mock_list_environments):
        """Test list environments command."""
        mock_list_environments.return_value = ["env1", "env2", "default"]

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

        assert result.exit_code == 0
        mock_list_environments.assert_called_once()
        # Check for Rich formatted output
        assert "Available Environments" in result.stdout
        assert "env1" in result.stdout
        assert "env2" in result.stdout
        assert "default" in result.stdout

    @patch("cli.list.list_model_organisms")
    def test_list_models_empty(self, mock_list_models):
        """Test list models command with empty result."""
        mock_list_models.return_value = []

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

        assert result.exit_code == 0
        mock_list_models.assert_called_once()
        # Should show user-friendly empty message
        assert "No model organisms found" in result.stdout

    def test_list_shows_help_without_args(self):
        """Test list command shows help when no arguments provided."""
        result = runner.invoke(app, ["list"])

        # Should show help instead of error due to no_args_is_help=True
        assert result.exit_code == 0
        assert "List available resources" in result.stdout
        assert "models" in result.stdout
        assert "detectors" in result.stdout
        assert "environments" in result.stdout


class TestCreateCommands:
    """Test create subcommands."""

    @patch("cli.create.create_new_model_organism")
    @patch("cli.list.list_model_organisms")
    def test_create_model_success(self, mock_list_models, mock_create_model):
        """Test successful create model command."""
        mock_list_models.return_value = []  # No existing models

        result = runner.invoke(
            app, ["create", "model", "--name", "test_model"])

        assert result.exit_code == 0
        mock_create_model.assert_called_once_with("test_model")
        assert "Successfully created model organism" in result.stdout
        assert "test_model" in result.stdout

    @patch("cli.create.create_new_model_organism")
    @patch("cli.list.list_model_organisms")
    def test_create_model_with_short_flag(self, mock_list_models, mock_create_model):
        """Test create model command with short -n flag."""
        mock_list_models.return_value = []

        result = runner.invoke(app, ["create", "model", "-n", "test_model"])

        assert result.exit_code == 0
        mock_create_model.assert_called_once_with("test_model")

    @patch("cli.list.list_model_organisms")
    def test_create_model_invalid_name(self, mock_list_models):
        """Test create model command with invalid name."""
        mock_list_models.return_value = []

        result = runner.invoke(
            app, ["create", "model", "--name", "invalid-name!"])

        assert result.exit_code != 0
        assert "Invalid name format" in result.stdout

    @patch("cli.create.create_new_model_organism")
    @patch("cli.list.list_model_organisms")
    def test_create_model_force_overwrite(self, mock_list_models, mock_create_model):
        """Test create model command with force overwrite."""
        mock_list_models.return_value = ["existing_model"]

        result = runner.invoke(
            app, ["create", "model", "--name", "existing_model", "--force"]
        )

        assert result.exit_code == 0
        mock_create_model.assert_called_once_with("existing_model")

    def test_create_shows_help_without_args(self):
        """Test create command shows help when no arguments provided."""
        result = runner.invoke(app, ["create"])

        # Should show help instead of error due to no_args_is_help=True
        assert result.exit_code == 0
        assert "Create new models, detectors, and environments" in result.stdout
        assert "model" in result.stdout
        assert "detector" in result.stdout
        assert "environment" in result.stdout


class TestCLIHelp:
    """Test CLI help and documentation."""

    def test_main_app_help(self):
        """Test main app help command."""
        result = runner.invoke(app, ["--help"])

        assert result.exit_code == 0
        assert "Bench-AF: Testbed for Alignment Faking Detection" in result.stdout
        assert "run" in result.stdout
        assert "list" in result.stdout
        assert "create" in result.stdout
        assert "Examples:" in result.stdout

    def test_main_app_shows_help_without_args(self):
        """Test main app shows help when no arguments provided."""
        result = runner.invoke(app, [])

        # Should show help instead of error due to no_args_is_help=True
        assert result.exit_code == 0
        assert "Bench-AF: Testbed for Alignment Faking Detection" in result.stdout

    def test_run_shows_help_without_args(self):
        """Test run command shows help when no arguments provided."""
        result = runner.invoke(app, ["run"])

        # Should show help instead of error due to no_args_is_help=True
        assert result.exit_code == 0
        assert "Execute validation and detection operations" in result.stdout
        assert "validate-model" in result.stdout
        assert "run-detector" in result.stdout

    def test_validate_model_help(self):
        """Test validate-model help command."""
        result = runner.invoke(app, ["run", "validate-model", "--help"])

        assert result.exit_code == 0
        assert "Validate a model organism on a specific environment" in result.stdout
        assert "--config" in result.stdout

    def test_run_detector_help(self):
        """Test run-detector help command."""
        result = runner.invoke(app, ["run", "run-detector", "--help"])

        assert result.exit_code == 0
        assert "Run a detector algorithm on a model organism" in result.stdout
        assert "--config" in result.stdout

    def test_list_help(self):
        """Test list subcommand help."""
        result = runner.invoke(app, ["list", "--help"])

        assert result.exit_code == 0
        assert "List available resources in the registry" in result.stdout
        assert "models" in result.stdout
        assert "detectors" in result.stdout
        assert "environments" in result.stdout

    def test_create_help(self):
        """Test create subcommand help."""
        result = runner.invoke(app, ["create", "--help"])

        assert result.exit_code == 0
        assert "Create new models, detectors, and environments" in result.stdout
        assert "model" in result.stdout
        assert "detector" in result.stdout
        assert "environment" in result.stdout

    def test_view_help(self):
        """Test view subcommand help."""
        result = runner.invoke(app, ["view", "validate-model"])
        assert result.exit_code == 0
        assert "Views all logs from model" in result.stdout


class TestCLIErrorHandling:
    """Test CLI error handling."""

    def test_invalid_command(self):
        """Test invalid command."""
        result = runner.invoke(app, ["invalid-command"])
        assert result.exit_code != 0

    def test_invalid_list_subcommand(self):
        """Test invalid list subcommand."""
        result = runner.invoke(app, ["list", "invalid"])
        assert result.exit_code != 0

    def test_invalid_run_subcommand(self):
        """Test invalid run subcommand."""
        result = runner.invoke(app, ["run", "invalid"])
        assert result.exit_code != 0

    def test_invalid_create_subcommand(self):
        """Test invalid create subcommand."""
        result = runner.invoke(app, ["create", "invalid"])
        assert result.exit_code != 0


class TestCLIIntegration:
    """Integration tests for CLI functionality."""

    @patch("cli.run.validate_model_organism")
    @patch("cli.run.load_environment")
    @patch("cli.run.load_model_organism")
    @patch("cli.run.load_config")
    @patch("cli.run._validate_config_path")
    @patch("cli.run._get_results_dir")
    def test_validate_model_integration(
        self,
        mock_get_results_dir,
        mock_validate_path,
        mock_load_config,
        mock_load_model,
        mock_load_env,
        mock_validate,
    ):
        """Test validate-model command integration."""
        # Setup realistic mocks
        mock_organism = MagicMock(spec=ModelOrganism)
        mock_organism.name = "test_model"
        mock_environment = MagicMock(spec=Environment)
        mock_environment.name = "test_env"
        mock_eval_logs = [MagicMock(spec=EvalLog)]
        mock_results_dir = Path("/tmp/mock_results")

        # Create mock config with new Pydantic structure
        mock_config = ValidateModelConfig(
            model_organism=ModelOrganismConfig(
                name="test_model", description="Test model organism", config={}
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={"test": "config"},
                )
            ],
        )

        mock_validate_path.return_value = Path(
            "test_config.json"
        )  # Mock successful validation
        mock_load_config.return_value = mock_config
        mock_load_model.return_value = mock_organism
        mock_load_env.return_value = mock_environment
        mock_validate.return_value = mock_eval_logs
        mock_get_results_dir.return_value = mock_results_dir

        # Test the full command
        result = runner.invoke(
            app, ["run", "validate-model", "--config", "test_config.json"]
        )

        assert result.exit_code == 0

        # Verify all functions were called correctly
        mock_load_config.assert_called_once_with(
            "test_config.json", "validate-model")
        mock_load_model.assert_called_once_with("test_model")
        mock_load_env.assert_called_once_with("test_env")
        mock_validate.assert_called_once_with(
            model_organism=mock_organism,
            environment=mock_environment,
            eval_config={"test": "config"},
            results_dir=mock_results_dir,
            raw_eval_logs=True,
        )

    @patch("cli.run.run_detector")
    @patch("cli.run.load_environment")
    @patch("cli.run.load_model_organism")
    @patch("cli.run.load_detector")
    @patch("cli.run.load_config")
    @patch("cli.run._validate_config_path")
    @patch("cli.run._get_results_dir")
    def test_run_detector_integration(
        self,
        mock_get_results_dir,
        mock_validate_path,
        mock_load_config,
        mock_load_detector,
        mock_load_model,
        mock_load_env,
        mock_run_detector,
    ):
        """Test run-detector command integration."""
        # Setup realistic mocks
        mock_detector = MagicMock(spec=Detector)
        mock_detector.name = "test_detector"
        mock_organism = MagicMock(spec=ModelOrganism)
        mock_organism.name = "test_model"
        mock_environment = MagicMock(spec=Environment)
        mock_environment.name = "test_env"
        mock_detector_result = (0.8, "High confidence detection")
        mock_results_dir = Path("/tmp/mock_results")

        # Create mock config with new Pydantic structure
        mock_config = RunDetectorConfig(
            detector=DetectorConfig(
                name="test_detector", description="Test detector"),
            model_organism=ModelOrganismConfig(
                name="test_model", description="Test model organism", config={}
            ),
            environments=[
                EnvironmentConfig(
                    name="test_env",
                    description="Test environment",
                    eval_config={"test": "config"},
                )
            ],
        )

        mock_validate_path.return_value = Path(
            "test_config.json"
        )  # Mock successful validation
        mock_load_config.return_value = mock_config
        mock_load_detector.return_value = mock_detector
        mock_load_model.return_value = mock_organism
        mock_load_env.return_value = mock_environment
        mock_run_detector.return_value = mock_detector_result
        mock_get_results_dir.return_value = mock_results_dir

        # Test the full command
        result = runner.invoke(
            app, ["run", "run-detector", "-c", "test_config.json"])

        assert result.exit_code == 0

        # Verify all functions were called correctly
        mock_load_config.assert_called_once_with(
            "test_config.json", "run-detector")
        mock_load_detector.assert_called_once_with("test_detector")
        mock_load_model.assert_called_once_with("test_model")
        mock_load_env.assert_called_once_with("test_env")
        mock_run_detector.assert_called_once_with(
            detector=mock_detector,
            model_organism=mock_organism,
            environment=mock_environment,
            eval_config={"test": "config"},
            results_dir=mock_results_dir,
        )

    def test_full_workflow_help_navigation(self):
        """Test navigating through help commands like a real user."""
        # Start with main help
        result = runner.invoke(app, [])
        assert result.exit_code == 0
        assert "run" in result.stdout

        # Navigate to run help
        result = runner.invoke(app, ["run"])
        assert result.exit_code == 0
        assert "validate-model" in result.stdout

        # Navigate to specific command help
        result = runner.invoke(app, ["run", "validate-model"])
        assert result.exit_code == 0
        assert "--config" in result.stdout

        # Navigate to list help
        result = runner.invoke(app, ["list"])
        assert result.exit_code == 0
        assert "models" in result.stdout

        # Navigate to create help
        result = runner.invoke(app, ["create"])
        assert result.exit_code == 0
        assert "model" in result.stdout
