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


from datetime import datetime, timezone
from typing import Any, Optional
from unittest.mock import MagicMock, patch

from httpx import AsyncClient, Response

from pontos.models import ModelError
from pontos.nvd.models.source import AcceptanceLevel
from pontos.nvd.source.api import MAX_SOURCES_PER_PAGE, SourceApi
from tests import AsyncMock, IsolatedAsyncioTestCase, aiter, anext
from tests.nvd import get_source_data


def create_source_response(
    name: str, update: Optional[dict[str, Any]] = None
) -> MagicMock:
    data = {
        "sources": [get_source_data({"name": name})],
        "results_per_page": 1,
    }
    if update:
        data.update(update)

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


def create_source_responses(count: int = 2) -> list[MagicMock]:
    return [
        create_source_response(f"MITRE-{i}", {"total_results": count})
        for i in range(1, count + 1)
    ]


class SourceApiTestCase(IsolatedAsyncioTestCase):
    @patch("pontos.nvd.api.AsyncClient", spec=AsyncClient)
    def setUp(self, async_client: MagicMock) -> None:
        self.http_client = AsyncMock()
        async_client.return_value = self.http_client
        self.api = SourceApi(token="token")

    async def test_sources(self):
        self.http_client.get.side_effect = create_source_responses()

        it = aiter(self.api.sources())
        source = await anext(it)

        self.assertEqual(source.name, "MITRE-1")
        self.assertEqual(
            source.contact_email,
            "cve@mitre.org",
        )
        self.assertEqual(
            source.source_identifiers,
            [
                "cve@mitre.org",
                "8254265b-2729-46b6-b9e3-3dfca2d5bfca",
            ],
        )
        self.assertEqual(
            source.last_modified,
            datetime(2019, 9, 9, 16, 18, 45, 930000, tzinfo=timezone.utc),
        )
        self.assertEqual(
            source.created,
            datetime(2019, 9, 9, 16, 18, 45, 930000, tzinfo=timezone.utc),
        )
        self.assertIsNone(source.v2_acceptance_level)
        self.assertEqual(
            source.v3_acceptance_level,
            AcceptanceLevel(
                "Contributor",
                datetime(2025, 1, 30, 0, 0, 20, 107000, tzinfo=timezone.utc),
            ),
        )
        self.assertEqual(
            source.cwe_acceptance_level,
            AcceptanceLevel(
                "Reference",
                datetime(2025, 1, 24, 0, 0, 0, 43000, tzinfo=timezone.utc),
            ),
        )

        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/source/2.0",
            headers={"apiKey": "token"},
            params={
                "startIndex": 0,
                "resultsPerPage": MAX_SOURCES_PER_PAGE,
            },
        )

        self.http_client.get.reset_mock()

        source = await anext(it)
        self.assertEqual(source.name, "MITRE-2")
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/source/2.0",
            headers={"apiKey": "token"},
            params={"startIndex": 1, "resultsPerPage": 1},
        )

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

    async def test_sources_change_dates(self):
        self.http_client.get.side_effect = create_source_responses()

        it = aiter(
            self.api.sources(
                last_modified_start_date=datetime(
                    2025, 1, 1, tzinfo=timezone.utc
                ),
                last_modified_end_date=datetime(
                    2025, 1, 31, tzinfo=timezone.utc
                ),
            )
        )
        source = await anext(it)

        self.assertEqual(source.name, "MITRE-1")
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/source/2.0",
            headers={"apiKey": "token"},
            params={
                "startIndex": 0,
                "lastModStartDate": "2025-01-01T00:00:00.000+00:00",
                "lastModEndDate": "2025-01-31T00:00:00.000+00:00",
                "resultsPerPage": MAX_SOURCES_PER_PAGE,
            },
        )

        self.http_client.get.reset_mock()

        source = await anext(it)

        self.assertEqual(source.name, "MITRE-2")
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/source/2.0",
            headers={"apiKey": "token"},
            params={
                "startIndex": 1,
                "lastModStartDate": "2025-01-01T00:00:00.000+00:00",
                "lastModEndDate": "2025-01-31T00:00:00.000+00:00",
                "resultsPerPage": 1,
            },
        )

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

    async def test_sources_source_identifier(self):
        self.http_client.get.side_effect = create_source_responses()

        it = aiter(self.api.sources(source_identifier="cve@mitre.org"))
        source = await anext(it)
        self.assertEqual(source.name, "MITRE-1")

        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/source/2.0",
            headers={"apiKey": "token"},
            params={
                "startIndex": 0,
                "resultsPerPage": MAX_SOURCES_PER_PAGE,
                "sourceIdentifier": "cve@mitre.org",
            },
        )

        self.http_client.get.reset_mock()

        source = await anext(it)
        self.assertEqual(source.name, "MITRE-2")
        self.http_client.get.assert_awaited_once_with(
            "https://services.nvd.nist.gov/rest/json/source/2.0",
            headers={"apiKey": "token"},
            params={
                "startIndex": 1,
                "resultsPerPage": 1,
                "sourceIdentifier": "cve@mitre.org",
            },
        )

        with self.assertRaises(StopAsyncIteration):
            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()

    async def test_sources_broken_response_return_exceptions(self):
        responses = create_source_responses(3)
        responses[1].json.return_value["sources"][0][
            "last_modified"
        ] = "I'm an invalid date"
        self.http_client.get.side_effect = responses

        it = aiter(self.api.sources(return_exceptions=True))

        source = await anext(it)
        self.assertEqual(source.name, "MITRE-1")

        source = await anext(it)
        self.assertIsInstance(source, ModelError)

        source = await anext(it)
        self.assertEqual(source.name, "MITRE-3")

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

    async def test_sources_broken_response(self):
        responses = create_source_responses(3)
        responses[1].json.return_value["sources"][0][
            "last_modified"
        ] = "I'm an invalid date"
        self.http_client.get.side_effect = responses

        it = aiter(self.api.sources(return_exceptions=False))

        source = await anext(it)
        self.assertEqual(source.name, "MITRE-1")

        with self.assertRaises(ModelError):
            await anext(it)
