# SPDX-FileCopyrightText: 2024 Greenbone AG
#
# SPDX-License-Identifier: GPL-3.0-or-later
#

# pylint: disable=line-too-long, arguments-differ, redefined-builtin
# ruff: noqa: E501

from datetime import datetime
from typing import Any, Optional
from unittest.mock import MagicMock, patch
from uuid import UUID, uuid4

from httpx import AsyncClient, Response

from pontos.errors import PontosError
from pontos.nvd.api import now
from pontos.nvd.cpe_match.api import MAX_CPE_MATCHES_PER_PAGE, CPEMatchApi
from tests import AsyncMock, IsolatedAsyncioTestCase, aiter, anext
from tests.nvd import get_cpe_match_data


def uuid_replace_str(uuid: UUID, iteration: int, number: int) -> str:
    id_str = str(uuid).rsplit("-", 2)
    return f"{id_str[0]}-{iteration:04}-{number:012}"


def uuid_replace(uuid: UUID, iteration: int, number: int) -> UUID:
    return UUID(uuid_replace_str(uuid, iteration, number))


def generate_cpe_name(iteration: int, number: int) -> str:
    return f"cpe:2.3:a:acme:test-app:1.{iteration-1}.{number-1}:*:*:*:*:*:*:*"


def create_cpe_match_response(
    match_criteria_id: UUID,
    cpe_name_id: UUID,
    *,
    update: Optional[dict[str, Any]] = None,
    results: int = 1,
    iteration: int = 1,
) -> MagicMock:
    match_strings = [
        {
            "match_string": get_cpe_match_data(
                {
                    "match_criteria_id": f"{uuid_replace_str(match_criteria_id, iteration, i)}",
                    "criteria": generate_cpe_name(iteration, i),
                    "matches": [
                        {
                            "cpe_name": generate_cpe_name(iteration, i),
                            "cpe_name_id": f"{uuid_replace_str(cpe_name_id, iteration, i)}",
                        }
                    ],
                }
            )
        }
        for i in range(1, results + 1)
    ]

    data = {
        "match_strings": match_strings,
        "results_per_page": results,
    }
    if update:
        data.update(update)

    response = MagicMock(spec=Response)
    response.json.return_value = data
    return response


def create_cpe_match_responses(
    match_criteria_id: UUID,
    cpe_name_id: UUID,
    responses: int = 2,
    results_per_response: int = 1,
) -> list[MagicMock]:
    return [
        create_cpe_match_response(
            match_criteria_id=match_criteria_id,
            cpe_name_id=cpe_name_id,
            update={"total_results": responses * results_per_response},
            results=results_per_response,
            iteration=i,
        )
        for i in range(1, responses + 1)
    ]


class CPEMatchApiTestCase(IsolatedAsyncioTestCase):
    @patch("pontos.nvd.api.time.monotonic", autospec=True)
    @patch("pontos.nvd.api.AsyncClient", spec=AsyncClient)
    def setUp(self, async_client: MagicMock, monotonic_mock: MagicMock) -> None:
        self.http_client = AsyncMock()
        async_client.return_value = self.http_client
        monotonic_mock.return_value = 0
        self.api = CPEMatchApi()

    async def test_no_match_criteria_id(self):
        with self.assertRaises(PontosError):
            await self.api.cpe_match(None)

    async def test_no_match_strings(self):
        data = {
            "match_strings": [],
            "results_per_page": 1,
        }
        response = MagicMock(spec=Response)
        response.json.return_value = data
        self.http_client.get.return_value = response

        with self.assertRaises(PontosError):
            await self.api.cpe_match("DOES-NOT-EXIST")

    async def test_cpe_match(self):
        match_criteria_id = uuid_replace(uuid4(), 1, 1)
        cpe_name_id = uuid_replace(uuid4(), 1, 1)

        self.http_client.get.return_value = create_cpe_match_response(
            match_criteria_id, cpe_name_id
        )

        cpe_match_string = await self.api.cpe_match(match_criteria_id)

        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={"matchCriteriaId": str(match_criteria_id)},
        )

        self.assertEqual(
            match_criteria_id,
            cpe_match_string.match_criteria_id,
        )
        self.assertEqual(
            generate_cpe_name(1, 1),
            cpe_match_string.criteria,
        )
        self.assertEqual(
            cpe_name_id,
            cpe_match_string.matches[0].cpe_name_id,
        )
        self.assertEqual(
            generate_cpe_name(1, 1),
            cpe_match_string.matches[0].cpe_name,
        )

    @patch("pontos.nvd.cpe_match.api.now", spec=now)
    async def test_cpe_matches_last_modified_start_date(
        self, now_mock: MagicMock
    ):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()

        now_mock.return_value = datetime(2019, 8, 30)
        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id, cpe_name_id
        )

        it = aiter(
            self.api.cpe_matches(last_modified_start_date=datetime(2019, 6, 1))
        )
        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 0,
                "lastModStartDate": "2019-06-01T00:00:00",
                "lastModEndDate": "2019-08-30T00:00:00",
                "resultsPerPage": MAX_CPE_MATCHES_PER_PAGE,
            },
        )

        self.http_client.get.reset_mock()

        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 1,
                "lastModStartDate": "2019-06-01T00:00:00",
                "lastModEndDate": "2019-08-30T00:00:00",
                "resultsPerPage": 1,
            },
        )

        with self.assertRaises(StopAsyncIteration):
            cpe_match = await anext(it)

    @patch("pontos.nvd.cpe_match.api.now", spec=now)
    async def test_cpe_matches_last_modified_end_date(
        self, now_mock: MagicMock
    ):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()

        now_mock.return_value = datetime(2019, 8, 30)
        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id, cpe_name_id
        )

        it = aiter(
            self.api.cpe_matches(last_modified_end_date=datetime(2019, 8, 1))
        )
        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 0,
                "lastModEndDate": "2019-08-01T00:00:00",
                "resultsPerPage": MAX_CPE_MATCHES_PER_PAGE,
            },
        )

        self.http_client.get.reset_mock()

        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 1,
                "lastModEndDate": "2019-08-01T00:00:00",
                "resultsPerPage": 1,
            },
        )

        with self.assertRaises(StopAsyncIteration):
            cpe_match = await anext(it)

    async def test_cpe_matches_cve_id(self):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()

        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id, cpe_name_id
        )

        it = aiter(self.api.cpe_matches(cve_id="CVE-2010-3574"))
        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 0,
                "cveId": "CVE-2010-3574",
                "resultsPerPage": MAX_CPE_MATCHES_PER_PAGE,
            },
        )

        self.http_client.get.reset_mock()

        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 1,
                "cveId": "CVE-2010-3574",
                "resultsPerPage": 1,
            },
        )

        with self.assertRaises(StopAsyncIteration):
            cpe_match = await anext(it)

    async def test_cpe_matches_match_string_search(self):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()

        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id, cpe_name_id
        )

        it = aiter(
            self.api.cpe_matches(
                match_string_search="cpe:2.3:a:sun:jre:*:*:*:*:*:*:*:*"
            )
        )
        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 0,
                "matchStringSearch": "cpe:2.3:a:sun:jre:*:*:*:*:*:*:*:*",
                "resultsPerPage": MAX_CPE_MATCHES_PER_PAGE,
            },
        )

        self.http_client.get.reset_mock()

        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 1,
                "matchStringSearch": "cpe:2.3:a:sun:jre:*:*:*:*:*:*:*:*",
                "resultsPerPage": 1,
            },
        )

        with self.assertRaises(StopAsyncIteration):
            cpe_match = await anext(it)

    async def test_cpe_matches_request_results(self):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()

        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id=match_criteria_id,
            cpe_name_id=cpe_name_id,
            results_per_response=2,
        )

        it = aiter(self.api.cpe_matches(request_results=10))
        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 0,
                "resultsPerPage": 10,
            },
        )

        self.http_client.get.reset_mock()
        cpe_match = await anext(it)
        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 2), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_not_called()

        self.http_client.get.reset_mock()

        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={},
            params={
                "startIndex": 2,
                "resultsPerPage": 2,
            },
        )

        self.http_client.get.reset_mock()
        cpe_match = await anext(it)
        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 2), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_not_called()

        with self.assertRaises(Exception):
            cpe_match = await anext(it)

    async def test_context_manager(self):
        async with self.api:
            pass

        self.http_client.__aenter__.assert_awaited_once()
        self.http_client.__aexit__.assert_awaited_once()


class CPEMatchApiWithTokenTestCase(IsolatedAsyncioTestCase):
    @patch("pontos.nvd.api.time.monotonic", autospec=True)
    @patch("pontos.nvd.api.AsyncClient", spec=AsyncClient)
    def setUp(self, async_client: MagicMock, monotonic_mock: MagicMock) -> None:
        self.http_client = AsyncMock()
        async_client.return_value = self.http_client
        monotonic_mock.return_value = 0
        self.api = CPEMatchApi(token="token123")

    async def test_cpe_matches_request_results_with_token(self):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()

        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id=match_criteria_id,
            cpe_name_id=cpe_name_id,
            results_per_response=2,
        )

        it = aiter(self.api.cpe_matches(request_results=10))
        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={"apiKey": "token123"},
            params={
                "startIndex": 0,
                "resultsPerPage": 10,
            },
        )

        self.http_client.get.reset_mock()
        cpe_match = await anext(it)
        self.assertEqual(
            uuid_replace(match_criteria_id, 1, 2), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_not_called()

        self.http_client.get.reset_mock()

        cpe_match = await anext(it)

        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 1), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/cpematch/2.0",
            headers={"apiKey": "token123"},
            params={
                "startIndex": 2,
                "resultsPerPage": 2,
            },
        )

        self.http_client.get.reset_mock()
        cpe_match = await anext(it)
        self.assertEqual(
            uuid_replace(match_criteria_id, 2, 2), cpe_match.match_criteria_id
        )
        self.http_client.get.assert_not_called()

        with self.assertRaises(Exception):
            cpe_match = await anext(it)

    @patch("pontos.nvd.api.time.monotonic", autospec=True)
    @patch("pontos.nvd.api.asyncio.sleep", autospec=True)
    async def test_rate_limit_with_token(
        self,
        sleep_mock: MagicMock,
        monotonic_mock: MagicMock,
    ):
        match_criteria_id = uuid4()
        cpe_name_id = uuid4()
        self.http_client.get.side_effect = create_cpe_match_responses(
            match_criteria_id, cpe_name_id, 8
        )
        monotonic_mock.side_effect = [10, 11]

        it = aiter(self.api.cpe_matches())
        await anext(it)
        await anext(it)
        await anext(it)
        await anext(it)
        await anext(it)

        sleep_mock.assert_not_called()

        await anext(it)

        sleep_mock.assert_not_called()
