"""Comprehensive CLI tests focused on improving code coverage."""

import json
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch

import pytest
from click.testing import CliRunner

from adversary_mcp_server.cli import _display_scan_results, _save_results_to_file, cli
from adversary_mcp_server.credential_manager import SecurityConfig
from adversary_mcp_server.threat_engine import Category, Language, Severity, ThreatMatch


class TestCLICommandsCoverage:
    """Test CLI commands to improve coverage."""

    def setup_method(self):
        """Setup test fixtures."""
        self.runner = CliRunner()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_configure_command_basic(self, mock_console, mock_cred_manager):
        """Test configure command with basic options."""
        mock_manager = Mock()
        mock_config = SecurityConfig()
        mock_manager.load_config.return_value = mock_config
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(
            cli,
            [
                "configure",
                "--openai-api-key",
                "sk-test123",
                "--severity-threshold",
                "high",
                "--enable-safety-mode",
                "--enable-llm",
            ],
        )

        assert result.exit_code == 0
        mock_manager.store_config.assert_called_once()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_configure_command_with_existing_config(
        self, mock_console, mock_cred_manager
    ):
        """Test configure command with existing configuration."""
        mock_manager = Mock()
        mock_config = SecurityConfig(openai_api_key="existing-key")
        mock_manager.load_config.return_value = mock_config
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(
            cli,
            [
                "configure",
                "--openai-api-key",
                "new-key",
                "--severity-threshold",
                "critical",
                "--disable-safety-mode",
                "--disable-llm",
            ],
        )

        assert result.exit_code == 0
        mock_manager.store_config.assert_called_once()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_configure_command_error_handling(self, mock_console, mock_cred_manager):
        """Test configure command error handling."""
        mock_manager = Mock()
        mock_manager.load_config.side_effect = Exception("Load failed")
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(
            cli,
            [
                "configure",
                "--openai-api-key",
                "sk-test123",
                "--severity-threshold",
                "medium",
            ],
        )

        # Should still work with default config
        assert result.exit_code == 0

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_configure_command_store_error(self, mock_console, mock_cred_manager):
        """Test configure command with store error."""
        mock_manager = Mock()
        mock_config = SecurityConfig()
        mock_manager.load_config.return_value = mock_config
        mock_manager.store_config.side_effect = Exception("Store failed")
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(
            cli, ["configure", "--openai-api-key", "sk-test123"]
        )

        assert result.exit_code == 1

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_status_command_configured(
        self, mock_console, mock_threat_engine, mock_cred_manager
    ):
        """Test status command with configured system."""
        mock_manager = Mock()
        mock_config = SecurityConfig(openai_api_key="sk-test123")
        mock_manager.load_config.return_value = mock_config
        mock_manager.has_config.return_value = True
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_engine.list_rules.return_value = [
            {"id": "rule1", "languages": ["python"]},
            {"id": "rule2", "languages": ["javascript", "typescript"]},
        ]
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["status"])

        assert result.exit_code == 0
        mock_console.print.assert_called()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_status_command_not_configured(
        self, mock_console, mock_threat_engine, mock_cred_manager
    ):
        """Test status command with unconfigured system."""
        mock_manager = Mock()
        mock_config = SecurityConfig()
        mock_manager.load_config.return_value = mock_config
        mock_manager.has_config.return_value = False
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_engine.list_rules.return_value = []
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["status"])

        assert result.exit_code == 0

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_status_command_error(self, mock_console, mock_cred_manager):
        """Test status command with error."""
        mock_manager = Mock()
        mock_manager.load_config.side_effect = Exception("Load failed")
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(cli, ["status"])

        assert result.exit_code == 1

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_list_rules_command_basic(self, mock_console, mock_threat_engine):
        """Test list_rules command basic functionality."""
        mock_engine = Mock()
        mock_engine.list_rules.return_value = [
            {
                "id": "sql_injection",
                "name": "SQL Injection",
                "category": "injection",
                "severity": "critical",
                "languages": ["python", "javascript"],
            }
        ]
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["list-rules"])

        assert result.exit_code == 0
        mock_console.print.assert_called()

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_list_rules_command_with_filters(self, mock_console, mock_threat_engine):
        """Test list_rules command with filters."""
        mock_engine = Mock()
        mock_engine.list_rules.return_value = [
            {
                "id": "sql_injection",
                "name": "SQL Injection",
                "category": "injection",
                "severity": "critical",
                "languages": ["python"],
            },
            {
                "id": "xss",
                "name": "XSS",
                "category": "xss",
                "severity": "high",
                "languages": ["javascript"],
            },
        ]
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(
            cli,
            [
                "list-rules",
                "--category",
                "injection",
                "--severity",
                "high",
                "--language",
                "python",
            ],
        )

        assert result.exit_code == 0

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_list_rules_command_with_output(self, mock_console, mock_threat_engine):
        """Test list_rules command with output file."""
        mock_engine = Mock()
        mock_engine.list_rules.return_value = [
            {
                "id": "test_rule",
                "name": "Test Rule",
                "category": "injection",
                "severity": "medium",
                "languages": ["python"],
            }
        ]
        mock_threat_engine.return_value = mock_engine

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            output_file = f.name

        try:
            result = self.runner.invoke(cli, ["list-rules", "--output", output_file])

            assert result.exit_code == 0

            # Verify file was created
            assert Path(output_file).exists()
            with open(output_file, "r") as f:
                data = json.load(f)
            assert len(data) == 1
            assert data[0]["id"] == "test_rule"

        finally:
            os.unlink(output_file)

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_list_rules_command_error(self, mock_console, mock_threat_engine):
        """Test list_rules command error handling."""
        mock_engine = Mock()
        mock_engine.list_rules.side_effect = Exception("Rules failed")
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["list-rules"])

        assert result.exit_code == 1

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_rule_details_command_found(self, mock_console, mock_threat_engine):
        """Test rule_details command for existing rule."""
        mock_rule = Mock()
        mock_rule.id = "test_rule"
        mock_rule.name = "Test Rule"
        mock_rule.category = Category.INJECTION
        mock_rule.severity = Severity.HIGH
        mock_rule.languages = [Language.PYTHON]
        mock_rule.description = "Test description"
        mock_rule.remediation = "Test remediation"
        mock_rule.cwe_id = "CWE-89"
        mock_rule.owasp_category = "A03"
        mock_rule.conditions = []
        mock_rule.exploit_templates = []
        mock_rule.references = ["http://example.com"]
        mock_rule.tags = None

        mock_engine = Mock()
        mock_engine.get_rule_by_id.return_value = mock_rule
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["rule-details", "test_rule"])

        assert result.exit_code == 0
        mock_console.print.assert_called()

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_rule_details_command_not_found(self, mock_console, mock_threat_engine):
        """Test rule_details command for non-existing rule."""
        mock_engine = Mock()
        mock_engine.get_rule_by_id.return_value = None
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["rule-details", "nonexistent_rule"])

        assert result.exit_code == 1

    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.console")
    def test_rule_details_command_error(self, mock_console, mock_threat_engine):
        """Test rule_details command error handling."""
        mock_engine = Mock()
        mock_engine.get_rule_by_id.side_effect = Exception("Rule details failed")
        mock_threat_engine.return_value = mock_engine

        result = self.runner.invoke(cli, ["rule-details", "test_rule"])

        assert result.exit_code == 1

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_reset_command_confirmed(self, mock_console, mock_cred_manager):
        """Test reset command with confirmation."""
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(cli, ["reset"], input="y\n")

        assert result.exit_code == 0
        mock_manager.delete_config.assert_called_once()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_reset_command_cancelled(self, mock_console, mock_cred_manager):
        """Test reset command cancelled."""
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(cli, ["reset"], input="n\n")

        assert result.exit_code == 0
        mock_manager.delete_config.assert_not_called()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.console")
    def test_reset_command_error(self, mock_console, mock_cred_manager):
        """Test reset command error handling."""
        mock_manager = Mock()
        mock_manager.delete_config.side_effect = Exception("Delete failed")
        mock_cred_manager.return_value = mock_manager

        result = self.runner.invoke(cli, ["reset"], input="y\n")

        assert result.exit_code == 1


class TestCLIScanCommand:
    """Test CLI scan command comprehensively."""

    def setup_method(self):
        """Setup test fixtures."""
        self.runner = CliRunner()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.ASTScanner")
    @patch("adversary_mcp_server.cli.console")
    def test_scan_file_basic(
        self,
        mock_console,
        mock_scanner,
        mock_threat_engine,
        mock_cred_manager,
    ):
        """Test scanning a single file."""
        # Setup mocks
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_threat_engine.return_value = mock_engine

        mock_scanner_instance = Mock()
        mock_scanner.return_value = mock_scanner_instance

        threat = ThreatMatch(
            rule_id="test_rule",
            rule_name="Test Rule",
            description="Test description",
            category=Category.INJECTION,
            severity=Severity.HIGH,
            file_path="test.py",
            line_number=1,
        )
        mock_scanner_instance.scan_code.return_value = [threat]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write("print('hello')")
            test_file = f.name

        try:
            result = self.runner.invoke(
                cli, ["scan", test_file, "--language", "python", "--severity", "medium"]
            )

            assert result.exit_code == 0
            mock_scanner_instance.scan_code.assert_called_once()

        finally:
            os.unlink(test_file)

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.ASTScanner")
    @patch("adversary_mcp_server.cli.console")
    def test_scan_directory_basic(
        self,
        mock_console,
        mock_scanner,
        mock_threat_engine,
        mock_cred_manager,
    ):
        """Test scanning a directory."""
        # Setup mocks
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_threat_engine.return_value = mock_engine

        mock_scanner_instance = Mock()
        mock_scanner.return_value = mock_scanner_instance
        mock_scanner_instance.scan_code.return_value = []

        with tempfile.TemporaryDirectory() as temp_dir:
            # Create a test file in the directory
            test_file = Path(temp_dir) / "test.py"
            with open(test_file, "w") as f:
                f.write("print('test')")

            result = self.runner.invoke(
                cli, ["scan", temp_dir, "--recursive", "--severity", "low"]
            )

            assert result.exit_code == 0
            # CLI calls scan_code for each file, not scan_directory
            mock_scanner_instance.scan_code.assert_called()

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.ASTScanner")
    @patch("adversary_mcp_server.cli.console")
    def test_scan_with_exploits(
        self,
        mock_console,
        mock_scanner,
        mock_threat_engine,
        mock_cred_manager,
    ):
        """Test scanning with exploit generation."""
        # Setup mocks
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_threat_engine.return_value = mock_engine

        mock_scanner_instance = Mock()
        mock_scanner.return_value = mock_scanner_instance

        threat = ThreatMatch(
            rule_id="test_rule",
            rule_name="Test Rule",
            description="Test description",
            category=Category.INJECTION,
            severity=Severity.HIGH,
            file_path="test.py",
            line_number=1,
        )
        mock_scanner_instance.scan_code.return_value = [threat]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write("print('hello')")
            test_file = f.name

        try:
            result = self.runner.invoke(
                cli, ["scan", test_file, "--include-exploits", "--use-llm"]
            )

            assert result.exit_code == 0
            # Note: CLI scan command doesn't currently implement exploit generation
            # so we don't assert on exploit_generator calls

        finally:
            os.unlink(test_file)

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.ASTScanner")
    @patch("adversary_mcp_server.cli.console")
    @patch("adversary_mcp_server.cli._save_results_to_file")
    def test_scan_with_output_file(
        self,
        mock_save_results,
        mock_console,
        mock_scanner,
        mock_threat_engine,
        mock_cred_manager,
    ):
        """Test scanning with output file."""
        # Setup mocks
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_threat_engine.return_value = mock_engine

        mock_scanner_instance = Mock()
        mock_scanner.return_value = mock_scanner_instance

        threat = ThreatMatch(
            rule_id="test_rule",
            rule_name="Test Rule",
            description="Test description",
            category=Category.INJECTION,
            severity=Severity.HIGH,
            file_path="test.py",
            line_number=1,
        )
        mock_scanner_instance.scan_code.return_value = [threat]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write("print('hello')")
            test_file = f.name

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            output_file = f.name

        try:
            result = self.runner.invoke(
                cli, ["scan", test_file, "--output", output_file]
            )

            assert result.exit_code == 0
            mock_save_results.assert_called_once()

        finally:
            os.unlink(test_file)
            os.unlink(output_file)

    @patch("adversary_mcp_server.cli.CredentialManager")
    @patch("adversary_mcp_server.cli.ThreatEngine")
    @patch("adversary_mcp_server.cli.ASTScanner")
    @patch("adversary_mcp_server.cli.console")
    def test_scan_command_with_mocked_error(
        self, mock_console, mock_scanner, mock_threat_engine, mock_cred_manager
    ):
        """Test scan command with mocked scanner error."""
        # Setup mocks
        mock_manager = Mock()
        mock_cred_manager.return_value = mock_manager

        mock_engine = Mock()
        mock_threat_engine.return_value = mock_engine

        mock_scanner_instance = Mock()
        mock_scanner_instance.scan_code.side_effect = Exception("Scanner error")
        mock_scanner.return_value = mock_scanner_instance

        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write("print('hello')")
            test_file = f.name

        try:
            result = self.runner.invoke(cli, ["scan", test_file])

            # Should handle scanner errors gracefully
            assert result.exit_code == 1

        finally:
            os.unlink(test_file)


class TestCLIUtilityFunctions:
    """Test CLI utility functions for coverage."""

    def test_save_results_to_file_json(self):
        """Test saving results to JSON file."""
        threats = [
            ThreatMatch(
                rule_id="test_rule",
                rule_name="Test Rule",
                description="Test description",
                category=Category.INJECTION,
                severity=Severity.HIGH,
                file_path="test.py",
                line_number=1,
            )
        ]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            output_file = f.name

        try:
            _save_results_to_file(threats, output_file)

            # Verify file was created
            assert Path(output_file).exists()
            with open(output_file, "r") as f:
                data = json.load(f)
            assert len(data) == 1
            assert data[0]["rule_id"] == "test_rule"

        finally:
            os.unlink(output_file)

    def test_save_results_to_file_text(self):
        """Test saving results to text file."""
        threats = [
            ThreatMatch(
                rule_id="test_rule",
                rule_name="Test Rule",
                description="Test description",
                category=Category.INJECTION,
                severity=Severity.HIGH,
                file_path="test.py",
                line_number=1,
            )
        ]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
            output_file = f.name

        try:
            _save_results_to_file(threats, output_file)

            # Verify file was created
            assert Path(output_file).exists()
            with open(output_file, "r") as f:
                content = f.read()
            assert "test_rule" in content
            assert "Test Rule" in content

        finally:
            os.unlink(output_file)

    @patch("adversary_mcp_server.cli.console")
    def test_display_scan_results_empty(self, mock_console):
        """Test displaying empty scan results."""
        _display_scan_results([], "test_target")
        mock_console.print.assert_called()

    @patch("adversary_mcp_server.cli.console")
    def test_display_scan_results_with_threats(self, mock_console):
        """Test displaying scan results with threats."""
        threats = [
            ThreatMatch(
                rule_id="test_rule",
                rule_name="Test Rule",
                description="Test description",
                category=Category.INJECTION,
                severity=Severity.HIGH,
                file_path="test.py",
                line_number=1,
                code_snippet="dangerous_code()",
                exploit_examples=["exploit1", "exploit2"],
            )
        ]

        _display_scan_results(threats, "test_target")
        mock_console.print.assert_called()
