import unittest

from pydantic import AnyUrl
from unittest.mock import MagicMock

from sator_core.models.patch import PatchReferences
from sator_core.models.vulnerability.references import VulnerabilityReferences
from sator_core.ports.driven.persistence.storage import StoragePersistencePort
from sator_core.ports.driven.repositories.vulnerability import VulnerabilityRepositoryPort
from sator_core.use_cases.resolution.references.vulnerability import VulnerabilityReferencesResolution


test_vulnerability_id = "CVE-2023-12345"

# Example references from the issue description
test_vulnerability_references = VulnerabilityReferences(
    vulnerability_id=test_vulnerability_id,
    advisories=[],
    exploits=[],
    reports=[],
    other=[
        AnyUrl("http://onlyoffice.com/"),
        AnyUrl("https://github.com/ONLYOFFICE/DocumentServer"),
        AnyUrl("https://github.com/ONLYOFFICE/core/blob/8ca40a44ce47a86168327a46db91253cf6bb205d/DesktopEditor/doctrenderer/"),
        AnyUrl("https://github.com/ONLYOFFICE/core/blob/8ca40a44ce47a86168327a46db91253cf6bb205d/DesktopEditor/doctrenderer/embed/NativeControlEmbed.cpp#L110")
    ]
)

test_patch_references = PatchReferences(
    vulnerability_id=test_vulnerability_id,
    diffs=[
        AnyUrl("https://github.com/ONLYOFFICE/core/commit/2b6ad83b36afd9845085b536969d366d1d61150a")
    ],
    messages=[
        AnyUrl("https://gist.github.com/merrychap/25eba8c4dd97c9e545edad1b8f0eadc2")
    ],
    other=[]
)

test_vulnerability_references_2 = VulnerabilityReferences(
    vulnerability_id=test_vulnerability_id,
    advisories=[
        AnyUrl("https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00954.html")
    ]
)

test_vulnerability_references_3 = VulnerabilityReferences(
    vulnerability_id=test_vulnerability_id,
    exploits=[
        AnyUrl("https://github.com/hayyp/cherry/issues/1")
    ]
)


class TestVulnerabilityReferencesResolution(unittest.TestCase):
    def setUp(self):
        self.mock_storage = MagicMock(spec=StoragePersistencePort)
        self.mock_repo1 = MagicMock(spec=VulnerabilityRepositoryPort)
        self.mock_repo2 = MagicMock(spec=VulnerabilityRepositoryPort)
        self.resolution = VulnerabilityReferencesResolution(
            vulnerability_repositories=[self.mock_repo1, self.mock_repo2],
            storage_port=self.mock_storage
        )

    def test_returns_cached_references(self):
        """Test returns cached references from storage"""
        self.mock_storage.load.side_effect = [test_vulnerability_references, None]  # Second None is for patch references

        result = self.resolution.search_vulnerability_references(test_vulnerability_id)

        self.assertEqual(result, test_vulnerability_references)
        self.mock_storage.load.assert_any_call(VulnerabilityReferences, test_vulnerability_id)
        self.mock_repo1.get_references.assert_not_called()
        self.mock_repo2.get_references.assert_not_called()

    def test_fetches_and_saves_new_references(self):
        """Test fetches references from repositories when not cached"""
        mock_refs1 = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id,
            advisories=test_vulnerability_references_2.advisories
        )
        mock_refs2 = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id,
            exploits=test_vulnerability_references_3.exploits
        )

        # First load returns None for vulnerability references
        self.mock_storage.load.side_effect = [None, None]  # Second None is for patch references
        self.mock_repo1.get_references.return_value = mock_refs1
        self.mock_repo2.get_references.return_value = mock_refs2

        # Mock patch references to be None
        self.mock_repo1.get_patch_references.return_value = None
        self.mock_repo2.get_patch_references.return_value = None

        result = self.resolution.search_vulnerability_references(test_vulnerability_id)

        # Expected result is the combined references from both repositories
        expected_result = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id,
            advisories=test_vulnerability_references_2.advisories,
            exploits=test_vulnerability_references_3.exploits
        )

        self.assertEqual(result, expected_result)
        self.mock_storage.save.assert_called_once_with(expected_result, test_vulnerability_id)
        self.mock_repo1.get_references.assert_called_once_with(test_vulnerability_id)
        self.mock_repo2.get_references.assert_called_once_with(test_vulnerability_id)

    def test_returns_none_when_no_references_found(self):
        """Test returns None when no repositories have references"""
        empty_refs = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id
        )

        # First load returns None for vulnerability references
        self.mock_storage.load.side_effect = [None, None]  # Second None is for patch references
        self.mock_repo1.get_references.return_value = empty_refs
        self.mock_repo2.get_references.return_value = empty_refs

        # Mock patch references to be None
        self.mock_repo1.get_patch_references.return_value = None
        self.mock_repo2.get_patch_references.return_value = None

        result = self.resolution.search_vulnerability_references(test_vulnerability_id)

        self.assertIsNone(result)
        self.mock_storage.save.assert_not_called()

    def test_handles_partial_repository_results(self):
        """Test combines results from repositories with partial responses"""
        mock_refs1 = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id,
            advisories=test_vulnerability_references_2.advisories
        )
        mock_refs2 = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id
        )

        # First load returns None for vulnerability references
        self.mock_storage.load.side_effect = [None, None]  # Second None is for patch references
        self.mock_repo1.get_references.return_value = mock_refs1
        self.mock_repo2.get_references.return_value = mock_refs2

        # Mock patch references to be None
        self.mock_repo1.get_patch_references.return_value = None
        self.mock_repo2.get_patch_references.return_value = None

        result = self.resolution.search_vulnerability_references(test_vulnerability_id)

        self.assertEqual(result, mock_refs1)
        self.mock_storage.save.assert_called_once_with(mock_refs1, test_vulnerability_id)

    def test_fetches_and_saves_patch_references(self):
        """Test fetches and saves patch references when not cached"""
        # First load returns None for vulnerability references
        # Second load returns None for patch references
        self.mock_storage.load.side_effect = [None, None]

        # Mock vulnerability references
        mock_vuln_refs = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id,
            advisories=test_vulnerability_references_2.advisories
        )
        self.mock_repo1.get_references.return_value = mock_vuln_refs
        self.mock_repo2.get_references.return_value = None

        # Mock patch references
        mock_patch_refs = PatchReferences(
            vulnerability_id=test_vulnerability_id,
            diffs=test_patch_references.diffs,
            messages=test_patch_references.messages
        )
        self.mock_repo1.get_patch_references.return_value = mock_patch_refs
        self.mock_repo2.get_patch_references.return_value = None

        result = self.resolution.search_vulnerability_references(test_vulnerability_id)

        # Verify vulnerability references were saved
        self.assertEqual(result, mock_vuln_refs)
        self.mock_storage.save.assert_any_call(mock_vuln_refs, test_vulnerability_id)

        # Verify patch references were saved
        self.mock_storage.save.assert_any_call(mock_patch_refs, test_vulnerability_id)

    def test_does_not_fetch_patch_references_when_cached(self):
        """Test does not fetch patch references when they are already cached"""
        # First load returns None for vulnerability references
        # Second load returns patch references (already cached)
        self.mock_storage.load.side_effect = [None, test_patch_references]

        # Mock vulnerability references
        mock_vuln_refs = VulnerabilityReferences(
            vulnerability_id=test_vulnerability_id,
            advisories=test_vulnerability_references_2.advisories
        )
        self.mock_repo1.get_references.return_value = mock_vuln_refs
        self.mock_repo2.get_references.return_value = None

        result = self.resolution.search_vulnerability_references(test_vulnerability_id)

        # Verify vulnerability references were saved
        self.assertEqual(result, mock_vuln_refs)
        self.mock_storage.save.assert_called_once_with(mock_vuln_refs, test_vulnerability_id)

        # Verify patch references methods were not called
        self.mock_repo1.get_patch_references.assert_not_called()
        self.mock_repo2.get_patch_references.assert_not_called()


if __name__ == "__main__":
    unittest.main()
