import json
import logging
import os
from pathlib import Path
from unittest import mock

import pytest
from aioresponses import aioresponses
from pytest_mock import MockerFixture

from checkov.common.bridgecrew.vulnerability_scanning.image_scanner import (
    ImageScanner,
    CHECKOV_SEC_IN_WEEK,
    DOCKER_IMAGE_SCAN_RESULT_FILE_NAME,
    redact_token_args,
)
from checkov.common.bridgecrew.vulnerability_scanning.integrations.docker_image_scanning import (
    docker_image_scanning_integration,
)


def get_report_url() -> str:
    base_url = "https://www.bridgecrew.cloud/api/v1/vulnerabilities"
    return f"{base_url}/results"


@pytest.mark.asyncio
async def test_report_results(mocker: MockerFixture, mock_bc_integration, docker_image_scan_result):
    # given
    bc_api_key = "abcd1234-abcd-1234-abcd-1234abcd1234"
    report_url = get_report_url()

    mocker.patch.dict(os.environ, {"BC_ROOT_DIR": "app"})

    # when
    with aioresponses() as m:
        m.post(report_url, status=200)

        result = await docker_image_scanning_integration.report_results_async(
            twistcli_scan_result=docker_image_scan_result,
            bc_platform_integration=mock_bc_integration,
            bc_api_key=bc_api_key,
            file_path=Path("app/Dockerfile"),
            file_content="FROM python:3.7-slim\n",
            docker_image_name="sha256:e70f7611f4d093d5f73026d23f7ab612aa1794abfb210ef1c549b225380d053b",
        )

    # then
    assert result == 0
    assert next(iter(m.requests.values()))[0].kwargs["json"] == {
        "dockerImageName": "sha256:e70f7611f4d093d5f73026d23f7ab612aa1794abfb210ef1c549b225380d053b",
        "dockerFilePath": "/Dockerfile",
        "dockerFileContent": "FROM python:3.7-slim\n",
        "type": "Image",
        "sourceId": "bridgecrewio/checkov",
        "branch": "master",
        "sourceType": "Github",
        "vulnerabilities": [
            {
                "cveId": "CVE-2022-23990",
                "status": "fixed in 2.2.10-2+deb11u1",
                "severity": "critical",
                "packageName": "expat",
                "packageVersion": "2.2.10-2",
                "link": "https://security-tracker.debian.org/tracker/CVE-2022-23990",
                "cvss": 9.8,
                "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
                "description": "Expat (aka libexpat) before 2.4.4 has an integer overflow in the doProlog function.",
                "riskFactors": [
                    "Attack complexity: low",
                    "Attack vector: network",
                    "Critical severity",
                    "Has fix",
                    "Recent vulnerability",
                ],
                "publishedDate": "2022-01-26T21:15:00+02:00",
            },
            {
                "cveId": "CVE-2022-23852",
                "status": "fixed in 2.2.10-2+deb11u1",
                "severity": "critical",
                "packageName": "expat",
                "packageVersion": "2.2.10-2",
                "link": "https://security-tracker.debian.org/tracker/CVE-2022-23852",
                "cvss": 9.8,
                "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
                "description": "Expat (aka libexpat) before 2.4.4 has a signed integer overflow in XML_GetBuffer, for configurations with a nonzero XML_CONTEXT_BYTES.",
                "riskFactors": [
                    "Attack complexity: low",
                    "Attack vector: network",
                    "Critical severity",
                    "Has fix",
                    "Recent vulnerability",
                ],
                "publishedDate": "2022-01-24T04:15:00+02:00",
            },
        ],
        "packages": [
            {"type": "os", "name": "mawk", "version": "1.3.4.20200120-2", "licenses": ["GPL-2"]},
            {"type": "os", "name": "gzip", "version": "1.10-4", "licenses": ["GPL-3+"]},
        ],
        "relatedResourceId": None,
        "errorLines": None
    }


@pytest.mark.asyncio
async def test_report_results_with_cicd(mocker: MockerFixture, mock_bc_integration, docker_image_scan_result):
    # given
    bc_api_key = "abcd1234-abcd-1234-abcd-1234abcd1234"
    report_url = get_report_url()
    cicd_details = {
        "runId": 123,
        "pr": "patch-1",
        "commit": "qwerty1234",
    }

    mock_bc_integration.cicd_details = cicd_details
    mocker.patch.dict(os.environ, {"BC_ROOT_DIR": "app"})

    # when
    with aioresponses() as m:
        m.post(report_url, status=200)

        result = await docker_image_scanning_integration.report_results_async(
            twistcli_scan_result=docker_image_scan_result,
            bc_platform_integration=mock_bc_integration,
            bc_api_key=bc_api_key,
            file_path=Path("app/Dockerfile"),
            file_content="FROM python:3.7-slim\n",
            docker_image_name="sha256:e70f7611f4d093d5f73026d23f7ab612aa1794abfb210ef1c549b225380d053b",
        )

    # then
    assert result == 0
    assert next(iter(m.requests.values()))[0].kwargs["json"]["cicdDetails"] == cicd_details


@pytest.mark.asyncio
async def test_report_results_fail(mocker: MockerFixture, mock_bc_integration, docker_image_scan_result):
    # given
    bc_api_key = "abcd1234-abcd-1234-abcd-1234abcd1234"
    report_url = get_report_url()

    mocker.patch.dict(os.environ, {"BC_ROOT_DIR": "app", "REQUEST_MAX_TRIES": "3", "SLEEP_BETWEEN_REQUEST_TRIES": "0.01"})

    # when
    with aioresponses() as m:
        m.post(report_url, status=403, repeat=True)

        result = await docker_image_scanning_integration.report_results_async(
            twistcli_scan_result=docker_image_scan_result,
            bc_platform_integration=mock_bc_integration,
            bc_api_key=bc_api_key,
            file_path=Path("app/Dockerfile"),
            file_content="FROM python:3.7-slim\n",
            docker_image_name="sha256:e70f7611f4d093d5f73026d23f7ab612aa1794abfb210ef1c549b225380d053b",
        )

    # then
    assert result == 1


def test_should_download_new_twistcli(tmp_path: Path):
    # given
    scanner = ImageScanner()
    twistcli_path = tmp_path / "twistcli"
    scanner.twistcli_path = twistcli_path

    # then
    assert scanner.should_download()


@mock.patch.dict(os.environ, {"CHECKOV_EXPIRATION_TIME_IN_SEC": str(CHECKOV_SEC_IN_WEEK)})
def test_not_should_download_twistcli(tmp_path: Path):
    # given
    scanner = ImageScanner()
    twistcli_path = tmp_path / "twistcli"
    twistcli_path.touch()
    scanner.twistcli_path = twistcli_path

    # then
    assert not scanner.should_download()


@mock.patch.dict(os.environ, {"CHECKOV_EXPIRATION_TIME_IN_SEC": "0"})
def test_should_download_twistcli_again(tmp_path: Path):
    # given
    scanner = ImageScanner()
    twistcli_path = tmp_path / "twistcli"
    twistcli_path.touch()
    scanner.twistcli_path = twistcli_path

    # then
    assert scanner.should_download()


def test_cleanup_twistcli_exists(tmp_path: Path):
    # given
    scanner = ImageScanner()

    # prepare local paths
    twistcli_path = tmp_path / "twistcli"
    twistcli_path.touch()
    scanner.twistcli_path = twistcli_path

    # when
    scanner.cleanup_scan()

    # then
    assert not twistcli_path.exists()


def test_cleanup_twistcli_not_exists(tmp_path: Path):
    # given
    scanner = ImageScanner()

    # prepare local paths
    twistcli_path = tmp_path / "twistcli"
    scanner.twistcli_path = twistcli_path

    # when
    scanner.cleanup_scan()

    # then
    assert not twistcli_path.exists()


def test_redact_token_args():
    args = ["twistcli", "images", "scan", "--address", "https://addr", "--token", "super-secret", "--details", "img"]
    redacted = redact_token_args(args)

    assert redacted[redacted.index("--token") + 1] == "***"
    assert "super-secret" not in redacted
    # original list must not be mutated
    assert args[args.index("--token") + 1] == "super-secret"


def test_run_image_scan_builds_list_args(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
):
    # given
    scanner = ImageScanner()
    scanner.twistcli_path = tmp_path / "twistcli"
    image_id = "alpine:3.19"
    api_key = "abcd1234-abcd-1234-abcd-1234abcd1234"

    monkeypatch.chdir(tmp_path)
    (tmp_path / DOCKER_IMAGE_SCAN_RESULT_FILE_NAME).write_text(json.dumps({"results": [{}]}))

    mocker.patch.object(docker_image_scanning_integration, "get_proxy_address", return_value="https://proxy.example")
    mocker.patch.object(docker_image_scanning_integration, "get_bc_api_key", return_value=api_key)
    run_mock = mocker.patch("checkov.common.bridgecrew.vulnerability_scanning.image_scanner.subprocess.run")

    # when
    with caplog.at_level(logging.DEBUG):
        scanner.run_image_scan(image_id)

    # then
    called_args = run_mock.call_args.args[0]
    assert isinstance(called_args, list)
    assert called_args.count(image_id) == 1
    assert called_args[-1] == image_id
    assert called_args[called_args.index("--output-file") + 1] == DOCKER_IMAGE_SCAN_RESULT_FILE_NAME
    assert api_key not in caplog.text
    assert "***" in caplog.text


def test_run_image_scan_rejects_invalid_image_id(tmp_path: Path, mocker: MockerFixture):
    # given
    scanner = ImageScanner()
    scanner.twistcli_path = tmp_path / "twistcli"
    run_mock = mocker.patch("checkov.common.bridgecrew.vulnerability_scanning.image_scanner.subprocess.run")

    # when / then
    with pytest.raises(ValueError):
        scanner.run_image_scan("alpine:3.19 --extra flag")

    run_mock.assert_not_called()
