# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Tests for CLI scan vuln and scan iac subcommands (DEV-873)."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from click.testing import CliRunner

from mergeguide.cli import main


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_vuln_scan_result(
    critical: int = 0,
    high: int = 0,
    medium: int = 0,
    low: int = 0,
) -> MagicMock:
    """Create a mock VulnerabilityScanner.ScanResult."""
    result = MagicMock()
    result.critical_count = critical
    result.high_count = high
    result.medium_count = medium
    result.low_count = low
    result.total_vulnerabilities = critical + high + medium + low
    result.total_dependencies = 10
    result.manifests_scanned = ["package.json"]
    result.findings = []
    result.to_dict.return_value = {
        "total_vulnerabilities": result.total_vulnerabilities,
        "critical_count": critical,
        "high_count": high,
    }
    return result


def _make_iac_scan_result(
    critical: int = 0,
    high: int = 0,
    medium: int = 0,
    low: int = 0,
    info: int = 0,
) -> MagicMock:
    """Create a mock IaCScanner.IaCScanResult."""
    result = MagicMock()
    result.critical_count = critical
    result.high_count = high
    result.medium_count = medium
    result.low_count = low
    result.info_count = info
    result.total_issues = critical + high + medium + low + info
    result.files_scanned = ["main.tf"]
    result.findings = []
    result.to_dict.return_value = {
        "total_issues": result.total_issues,
        "critical_count": critical,
        "high_count": high,
    }
    return result


# ---------------------------------------------------------------------------
# scan vuln tests
# ---------------------------------------------------------------------------


class TestScanVuln:
    """Tests for the `mergeguide scan vuln` command."""

    @patch("mergeguide.scanning.vulnerability_scanner.VulnerabilityScanner")
    def test_scan_vuln_happy_path_no_findings(self, MockScanner: MagicMock) -> None:
        """scan vuln with no vulnerabilities exits 0."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_vuln_scan_result()

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "vuln"])

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

    @patch("mergeguide.scanning.vulnerability_scanner.VulnerabilityScanner")
    def test_scan_vuln_happy_path_with_findings(self, MockScanner: MagicMock) -> None:
        """scan vuln with findings but --fail-on none exits 0."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_vuln_scan_result(
            critical=1, high=2
        )

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "vuln"])

        # Default --fail-on is none, so exit 0 even with findings
        assert result.exit_code == 0

    @patch("mergeguide.scanning.vulnerability_scanner.VulnerabilityScanner")
    def test_scan_vuln_json_format(self, MockScanner: MagicMock) -> None:
        """scan vuln --format json outputs valid JSON."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_vuln_scan_result()

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "vuln", "--format", "json"])

        assert result.exit_code == 0
        assert '"total_vulnerabilities"' in result.output

    @patch("mergeguide.scanning.vulnerability_scanner.VulnerabilityScanner")
    def test_scan_vuln_fail_on_critical_exits_1(self, MockScanner: MagicMock) -> None:
        """scan vuln --fail-on critical exits 1 when critical findings exist."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_vuln_scan_result(critical=1)

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "vuln", "--fail-on", "critical"])

        assert result.exit_code == 1

    @patch("mergeguide.scanning.vulnerability_scanner.VulnerabilityScanner")
    def test_scan_vuln_fail_on_critical_exits_0_for_high(
        self, MockScanner: MagicMock
    ) -> None:
        """scan vuln --fail-on critical exits 0 when only high findings exist."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_vuln_scan_result(high=3)

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "vuln", "--fail-on", "critical"])

        assert result.exit_code == 0

    @patch("mergeguide.scanning.vulnerability_scanner.VulnerabilityScanner")
    def test_scan_vuln_fail_on_high_exits_1(self, MockScanner: MagicMock) -> None:
        """scan vuln --fail-on high exits 1 when high findings exist."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_vuln_scan_result(high=2)

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "vuln", "--fail-on", "high"])

        assert result.exit_code == 1


# ---------------------------------------------------------------------------
# scan iac tests
# ---------------------------------------------------------------------------


class TestScanIac:
    """Tests for the `mergeguide scan iac` command."""

    @patch("mergeguide.scanning.iac_scanner.IaCScanner")
    def test_scan_iac_happy_path_no_findings(self, MockScanner: MagicMock) -> None:
        """scan iac with no issues exits 0."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_iac_scan_result()

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "iac"])

        assert result.exit_code == 0
        assert "No IaC security issues found" in result.output

    @patch("mergeguide.scanning.iac_scanner.IaCScanner")
    def test_scan_iac_happy_path_with_findings(self, MockScanner: MagicMock) -> None:
        """scan iac with findings but --fail-on none exits 0."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_iac_scan_result(
            critical=1, high=3
        )

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "iac"])

        assert result.exit_code == 0

    @patch("mergeguide.scanning.iac_scanner.IaCScanner")
    def test_scan_iac_json_format(self, MockScanner: MagicMock) -> None:
        """scan iac --format json outputs valid JSON."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_iac_scan_result()

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "iac", "--format", "json"])

        assert result.exit_code == 0
        assert '"total_issues"' in result.output

    @patch("mergeguide.scanning.iac_scanner.IaCScanner")
    def test_scan_iac_fail_on_critical_exits_1(self, MockScanner: MagicMock) -> None:
        """scan iac --fail-on critical exits 1 when critical findings exist."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_iac_scan_result(critical=2)

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "iac", "--fail-on", "critical"])

        assert result.exit_code == 1

    @patch("mergeguide.scanning.iac_scanner.IaCScanner")
    def test_scan_iac_fail_on_critical_exits_0_for_high(
        self, MockScanner: MagicMock
    ) -> None:
        """scan iac --fail-on critical exits 0 when only high findings exist."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_iac_scan_result(high=5)

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "iac", "--fail-on", "critical"])

        assert result.exit_code == 0

    @patch("mergeguide.scanning.iac_scanner.IaCScanner")
    def test_scan_iac_fail_on_high_exits_1(self, MockScanner: MagicMock) -> None:
        """scan iac --fail-on high exits 1 when high findings exist."""
        mock_instance = MockScanner.return_value
        mock_instance.scan_directory.return_value = _make_iac_scan_result(high=1)

        runner = CliRunner()
        result = runner.invoke(main, ["scan", "iac", "--fail-on", "high"])

        assert result.exit_code == 1
