"""Tests for complyform.core.validators.checkov_runner."""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import patch

import pytest

from complyform.core.errors import CheckovNotFoundError, ComplyFormError
from complyform.core.validators.checkov_runner import (
    CheckovRunner,
)

if TYPE_CHECKING:
    from pathlib import Path

FIXTURES = __import__("pathlib").Path(__file__).parent / "fixtures"


class TestSuccessfulRun:
    def test_parse_passing_output(self) -> None:
        raw = (FIXTURES / "checkov_output_pass.json").read_text()
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        results = runner._parse_output(raw)
        assert len(results) == 2
        assert all(r.status == "PASSED" for r in results)

    def test_parse_failing_output(self) -> None:
        raw = (FIXTURES / "checkov_output_fail.json").read_text()
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        results = runner._parse_output(raw)
        passed = [r for r in results if r.status == "PASSED"]
        failed = [r for r in results if r.status == "FAILED"]
        assert len(passed) == 1
        assert len(failed) == 1
        assert failed[0].check_id == "CKV_GCP_6"

    def test_result_fields(self) -> None:
        raw = (FIXTURES / "checkov_output_fail.json").read_text()
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        results = runner._parse_output(raw)
        failed = [r for r in results if r.status == "FAILED"][0]
        assert failed.check_name
        assert failed.resource
        assert failed.file_path
        assert failed.guideline is not None


class TestCheckovNotFound:
    def test_raises_when_not_available(self, tmp_path: Path) -> None:
        runner = CheckovRunner(checkov_path=None)
        with (
            patch("shutil.which", return_value=None),
            pytest.raises(CheckovNotFoundError),
        ):
            runner.run(tmp_path)


class TestTimeout:
    def test_timeout_raises_error(self, tmp_path: Path) -> None:
        import subprocess

        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        with (
            patch(
                "subprocess.run",
                side_effect=subprocess.TimeoutExpired("checkov", 30),
            ),
            pytest.raises(ComplyFormError, match="timed out"),
        ):
            runner.run(tmp_path, timeout=30)


class TestMalformedOutput:
    def test_bad_json_raises_error(self) -> None:
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        with pytest.raises(ComplyFormError, match="parse"):
            runner._parse_output("not json at all {{{")

    def test_empty_output_returns_empty(self) -> None:
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        results = runner._parse_output("")
        assert results == []

    def test_empty_json_object(self) -> None:
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        results = runner._parse_output("{}")
        assert results == []


class TestFilterByCheckIds:
    def test_check_ids_passed_to_command(self, tmp_path: Path) -> None:
        runner = CheckovRunner(checkov_path="/usr/bin/checkov")
        with (
            patch(
                "subprocess.run",
                return_value=type("R", (), {"stdout": "[]", "returncode": 0})(),
            ) as mock_run,
        ):
            runner.run(
                tmp_path,
                check_ids=["CKV_GCP_1", "CKV_GCP_6"],
            )
            cmd = mock_run.call_args[0][0]
            assert "--check" in cmd
            idx = cmd.index("--check")
            assert cmd[idx + 1] == "CKV_GCP_1,CKV_GCP_6"


class TestIsAvailable:
    def test_available_when_found(self) -> None:
        runner = CheckovRunner()
        with patch("shutil.which", return_value="/usr/bin/checkov"):
            assert runner.is_available() is True

    def test_not_available_when_missing(self) -> None:
        runner = CheckovRunner()
        with patch("shutil.which", return_value=None):
            assert runner.is_available() is False
