import copy
import datetime

import pytest

from anchore_engine.db.entities.policy_engine import (
    DistroMapping,
    DistroTuple,
    FixedArtifact,
    ImagePackage,
    ImagePackageVulnerability,
    Vulnerability,
    VulnerableArtifact,
)
from anchore_engine.subsys import logger

logger.enable_test_logging(level="DEBUG")


@pytest.fixture
def empty_vulnerability():
    v = Vulnerability()
    v.id = "CVE-1"
    v.namespace_name = "rhel:8"
    v.description = "test vulnerability"
    v.metadata_json = {}
    v.created_at = datetime.datetime.utcnow()
    v.updated_at = datetime.datetime.utcnow()
    v.fixed_in = []
    v.vulnerable_in = []
    v.severity = "high"
    v.link = "somelink"
    return v


@pytest.fixture
def empty_semver_vulnerability():
    v = Vulnerability()
    v.id = "CVE-2000"
    v.namespace_name = "github:npm"
    v.description = "test vulnerability for semver handling"
    v.metadata_json = {}
    v.created_at = datetime.datetime.utcnow()
    v.updated_at = datetime.datetime.utcnow()
    v.fixed_in = []
    v.vulnerable_in = []
    v.severity = "high"
    v.link = "somelink"
    return v


@pytest.fixture
def vulnerability_with_fix(empty_vulnerability):
    fixed_vuln = copy.deepcopy(empty_vulnerability)
    f = FixedArtifact()
    f.vulnerability_id = fixed_vuln.id
    f.name = "pkg1"
    f.namespace_name = fixed_vuln.namespace_name
    f.version = "0:1.1.el8"
    f.version_format = "RPM"
    f.parent = fixed_vuln
    f.include_later_versions = True
    f.epochless_version = f.version
    f.fix_metadata = {}
    f.created_at = datetime.datetime.now()
    f.updated_at = datetime.datetime.now()
    f.fix_observed_at = f.updated_at
    fixed_vuln.fixed_in = [f]
    return fixed_vuln


@pytest.fixture
def vulnerability_with_nofix(empty_vulnerability):
    fixed_vuln = copy.deepcopy(empty_vulnerability)
    f = FixedArtifact()
    f.vulnerability_id = fixed_vuln.id
    f.name = "pkg1"
    f.namespace_name = fixed_vuln.namespace_name
    f.version = "None"
    f.version_format = "RPM"
    f.parent = fixed_vuln
    f.include_later_versions = True
    f.epochless_version = f.version
    f.fix_metadata = {}
    f.created_at = datetime.datetime.now()
    f.updated_at = datetime.datetime.now()
    f.fix_observed_at = f.updated_at
    fixed_vuln.fixed_in = [f]
    return fixed_vuln


@pytest.fixture
def vulnerability_with_multifix(empty_semver_vulnerability):
    fixed_vuln = copy.deepcopy(empty_semver_vulnerability)
    f = FixedArtifact()
    f.vulnerability_id = fixed_vuln.id
    f.name = "semverpkg1"
    f.namespace_name = fixed_vuln.namespace_name
    f.version = ">= 1.1.0 < 1.1.2"
    f.version_format = "semver"
    f.parent = fixed_vuln
    f.include_later_versions = False
    f.epochless_version = f.version
    f.fix_metadata = {"first_patched_version": "1.1.2"}
    f.created_at = datetime.datetime.now()
    f.updated_at = datetime.datetime.now()
    f.fix_observed_at = f.updated_at
    fixed_vuln.fixed_in = [f]

    f = FixedArtifact()
    f.vulnerability_id = fixed_vuln.id
    f.name = "semverpkg1"
    f.namespace_name = fixed_vuln.namespace_name
    f.version = ">= 2.2.0 < 2.2.2"
    f.version_format = "semver"
    f.parent = fixed_vuln
    f.include_later_versions = False
    f.epochless_version = f.version
    f.fix_metadata = {"first_patched_version": "2.2.2"}
    f.created_at = datetime.datetime.now()
    f.updated_at = datetime.datetime.now()
    f.fix_observed_at = f.updated_at

    return fixed_vuln


@pytest.fixture
def vulnerability_with_vulnartifact(empty_vulnerability):
    vuln_art = copy.deepcopy(empty_vulnerability)
    v = VulnerableArtifact(
        vulnerability_id=vuln_art.id,
        namespace_name=vuln_art.namespace_name,
        name="pkg1",
        version="1.0.el8",
        parent=vuln_art,
    )
    v.epochless_version = "0:" + v.version
    v.version_format = "rpm"
    v.include_previous_versions = False
    vuln_art.vulnerable_in = [v]

    v = VulnerableArtifact(
        vulnerability_id=vuln_art.id,
        namespace_name=vuln_art.namespace_name,
        name="pkg1",
        version="0.9.el8",
        parent=vuln_art,
    )
    v.epochless_version = "0:" + v.version
    v.version_format = "rpm"
    v.include_previous_versions = False
    vuln_art.vulnerable_in.append(v)
    return vuln_art


@pytest.fixture
def vulnerability_with_both(vulnerability_with_fix, vulnerability_with_vulnartifact):
    vulnerability_with_fix.fixed_in[0].include_later_versions = False
    vulnerability_with_fix.vulnerable_in = vulnerability_with_vulnartifact.vulnerable_in
    return vulnerability_with_fix


@pytest.fixture
def nvd_vulnerability():
    """
    Returns a vulnerability similar to an NVD record but with an added fixed record, similar to how GitHub advisories have both vuln range and fix version
    :return:
    """
    v = Vulnerability()
    v.id = "CVE-2"
    v.created_at = v.updated_at = datetime.datetime.utcnow()
    v.severity = "high"
    v.namespace_name = "nvdv2:cves"


@pytest.fixture
def vulnerable_semver_pkg1():
    pkg = ImagePackage()
    pkg.image_id = "image1"
    pkg.image_user_id = "admin"
    pkg.name = "semverpkg1"
    pkg.normalized_src_pkg = "semverpkg1"
    pkg.version = "1.1.0"
    pkg.fullversion = "1.1.0"
    pkg.release = None
    pkg.pkg_type = "npm"
    pkg.distro_name = "npm"
    pkg.distro_version = "N/A"
    pkg.like_distro = "npm"
    pkg.arch = "amd64"
    pkg.pkg_path = "/app/myapp/package.json"
    return pkg


@pytest.fixture
def vulnerable_semver_pkg2():
    pkg = ImagePackage()
    pkg.image_id = "image1"
    pkg.image_user_id = "admin"
    pkg.name = "semverpkg1"
    pkg.normalized_src_pkg = "semverpkg1"
    pkg.version = "2.2.0"
    pkg.fullversion = "2.2.0"
    pkg.release = None
    pkg.pkg_type = "npm"
    pkg.distro_name = "npm"
    pkg.distro_version = "N/A"
    pkg.like_distro = "npm"
    pkg.arch = "amd64"
    pkg.pkg_path = "/app/myapp2/package.json"
    return pkg


@pytest.fixture
def vulnerable_pkg1():
    pkg = ImagePackage()
    pkg.image_id = "image1"
    pkg.image_user_id = "admin"
    pkg.name = "pkg1"
    pkg.normalized_src_pkg = "pkg1"
    pkg.version = "0:1.0.el8"
    pkg.fullversion = "0:1.0.el8"
    pkg.release = None
    pkg.pkg_type = "RPM"
    pkg.distro_name = "rhel"
    pkg.distro_version = "8"
    pkg.like_distro = "RHEL"
    pkg.arch = "amd64"
    pkg.pkg_path = "rpmdb"
    return pkg


@pytest.fixture
def nonvulnerable_pkg1():
    pkg = ImagePackage()
    pkg.image_id = "image1"
    pkg.image_user_id = "admin"
    pkg.name = "pkg1"
    pkg.normalized_src_pkg = "pkg1"
    pkg.version = "1.1.el8"
    pkg.fullversion = "0:1.1.el8"
    pkg.release = None
    pkg.pkg_type = "RPM"
    pkg.distro_name = "centos"
    pkg.distro_version = "8"
    pkg.like_distro = "RHEL"
    return pkg


@pytest.fixture
def python_pkg1_100():
    pkg = ImagePackage()
    pkg.image_id = "image1"
    pkg.image_user_id = "admin"
    pkg.name = "pythonpkg1"
    pkg.normalized_src_pkg = "pythonpkg1"
    pkg.version = "1.0.0"
    pkg.fullversion = "1.0.0"
    pkg.release = None
    pkg.pkg_type = "python"
    pkg.distro_name = "centos"
    pkg.distro_version = "8"
    pkg.like_distro = "RHEL"
    return pkg


@pytest.fixture
def python_pkg1_101():
    pkg = ImagePackage()
    pkg.image_id = "image1"
    pkg.image_user_id = "admin"
    pkg.name = "pythonpkg1"
    pkg.normalized_src_pkg = "pythonpkg1"
    pkg.version = "1.0.1"
    pkg.fullversion = "1.0.1"
    pkg.release = None
    pkg.pkg_type = "python"
    pkg.distro_name = "centos"
    pkg.distro_version = "8"
    pkg.like_distro = "RHEL"
    return pkg


def mock_distros_for(distro, version, like_distro=""):
    """
    Mock implementation that doesn't use db
    :param cls:
    :param distro:
    :param version:
    :param like_distro:
    :return:
    """
    logger.info("Calling mocked distro_for %s %s %s", distro, version, like_distro)
    return [DistroTuple(distro=distro, version=version, flavor=like_distro)]


@pytest.fixture
def monkeypatch_distros(monkeysession):
    """
    Creates a monkey patch for the distro lookup to avoid DB operations
    :return:
    """

    monkeysession.setattr(DistroMapping, "distros_for", mock_distros_for)


def test_fixed_match(
    vulnerability_with_fix, vulnerable_pkg1, nonvulnerable_pkg1, monkeypatch_distros
):
    """
    Test matches against fixed artifacts
    :return:
    """
    f = vulnerability_with_fix.fixed_in[0]
    logger.info("Testing package %s", vulnerable_pkg1)
    logger.info("Testing vuln %s", f)
    assert isinstance(f, FixedArtifact)
    assert f.match_but_not_fixed(vulnerable_pkg1)
    assert not f.match_but_not_fixed(nonvulnerable_pkg1)

    pkg_vuln = ImagePackageVulnerability()
    pkg_vuln.package = vulnerable_pkg1
    pkg_vuln.vulnerability = vulnerability_with_fix
    pkg_vuln.pkg_type = vulnerable_pkg1.name
    pkg_vuln.pkg_version = vulnerable_pkg1.version
    pkg_vuln.pkg_image_id = vulnerable_pkg1.image_id
    pkg_vuln.pkg_user_id = vulnerable_pkg1.image_user_id
    pkg_vuln.pkg_name = vulnerable_pkg1.name
    pkg_vuln.pkg_arch = vulnerable_pkg1.arch
    pkg_vuln.vulnerability_id = vulnerability_with_fix.id
    pkg_vuln.vulnerability_namespace_name = vulnerability_with_fix.namespace_name

    assert pkg_vuln.fixed_in() == f.version


def test_notfixed_match(vulnerability_with_nofix, vulnerable_pkg1, monkeypatch_distros):
    """
    Test matches against fixed artifacts
    :return:
    """
    f = vulnerability_with_nofix.fixed_in[0]
    logger.info("Testing package %s", vulnerable_pkg1)
    logger.info("Testing vuln %s", f)
    assert isinstance(f, FixedArtifact)
    assert f.match_but_not_fixed(vulnerable_pkg1)

    pkg_vuln = ImagePackageVulnerability()
    pkg_vuln.package = vulnerable_pkg1
    pkg_vuln.vulnerability = vulnerability_with_nofix
    pkg_vuln.pkg_type = vulnerable_pkg1.name
    pkg_vuln.pkg_version = vulnerable_pkg1.version
    pkg_vuln.pkg_image_id = vulnerable_pkg1.image_id
    pkg_vuln.pkg_user_id = vulnerable_pkg1.image_user_id
    pkg_vuln.pkg_name = vulnerable_pkg1.name
    pkg_vuln.pkg_arch = vulnerable_pkg1.arch
    pkg_vuln.vulnerability_id = vulnerability_with_nofix.id
    pkg_vuln.vulnerability_namespace_name = vulnerability_with_nofix.namespace_name

    assert pkg_vuln.fixed_in() is None


def test_vulnerable_in(
    vulnerability_with_vulnartifact,
    vulnerable_pkg1,
    nonvulnerable_pkg1,
    monkeypatch_distros,
):
    """
    Test vulnerable in matches
    :return:
    """

    f = vulnerability_with_vulnartifact.vulnerable_in[0]
    logger.info("Testing package %s", vulnerable_pkg1)
    logger.info("Testing vuln %s", f)
    assert isinstance(f, VulnerableArtifact)
    assert f.match_and_vulnerable(vulnerable_pkg1)
    assert not f.match_and_vulnerable(nonvulnerable_pkg1)

    f = vulnerability_with_vulnartifact.vulnerable_in[1]
    logger.info("Testing package %s", vulnerable_pkg1)
    logger.info("Testing vuln %s", f)
    assert isinstance(f, VulnerableArtifact)
    assert not f.match_and_vulnerable(
        vulnerable_pkg1
    )  # Both not vuln now, this entry is for 0.9.x
    assert not f.match_and_vulnerable(nonvulnerable_pkg1)

    pkg_vuln = ImagePackageVulnerability()
    pkg_vuln.package = vulnerable_pkg1
    pkg_vuln.vulnerability = vulnerability_with_vulnartifact
    pkg_vuln.pkg_type = vulnerable_pkg1.name
    pkg_vuln.pkg_version = vulnerable_pkg1.version
    pkg_vuln.pkg_image_id = vulnerable_pkg1.image_id
    pkg_vuln.pkg_user_id = vulnerable_pkg1.image_user_id
    pkg_vuln.pkg_name = vulnerable_pkg1.name
    pkg_vuln.pkg_arch = vulnerable_pkg1.arch
    pkg_vuln.vulnerability_id = vulnerability_with_vulnartifact.id
    pkg_vuln.vulnerability_namespace_name = (
        vulnerability_with_vulnartifact.namespace_name
    )

    assert pkg_vuln.fixed_in() == None


def test_fixed_and_vulnerable(
    vulnerability_with_both, vulnerable_pkg1, nonvulnerable_pkg1, monkeypatch_distros
):
    """
    Test both fixed and vulnerable matches
    :return:
    """
    f = vulnerability_with_both.fixed_in[0]
    v = vulnerability_with_both.vulnerable_in[0]
    logger.info("Testing package %s", vulnerable_pkg1)
    logger.info("Testing vuln %s", f)
    assert isinstance(v, VulnerableArtifact)
    assert v.match_and_vulnerable(vulnerable_pkg1)
    assert not v.match_and_vulnerable(nonvulnerable_pkg1)

    pkg_vuln = ImagePackageVulnerability()
    pkg_vuln.package = vulnerable_pkg1
    pkg_vuln.vulnerability = vulnerability_with_both
    pkg_vuln.pkg_type = vulnerable_pkg1.name
    pkg_vuln.pkg_version = vulnerable_pkg1.version
    pkg_vuln.pkg_image_id = vulnerable_pkg1.image_id
    pkg_vuln.pkg_user_id = vulnerable_pkg1.image_user_id
    pkg_vuln.pkg_name = vulnerable_pkg1.name
    pkg_vuln.pkg_arch = vulnerable_pkg1.arch
    pkg_vuln.vulnerability_id = vulnerability_with_both.id
    pkg_vuln.vulnerability_namespace_name = vulnerability_with_both.namespace_name

    assert pkg_vuln.fixed_in() == "0:1.1.el8"


def test_non_comparable_versions(python_pkg1_100, python_pkg1_101, monkeypatch_distros):
    """
    Tests matching where fixed and vuln records use a version format that doesn't support comparators beyond equality (e.g CPEs)
    :return:
    """
    assert isinstance(python_pkg1_100, ImagePackage)
    assert isinstance(python_pkg1_101, ImagePackage)

    v1 = Vulnerability()
    v1.id = "CVE-100"
    v1.namespace_name = "nvdv2:cves"
    v1.severity = "high"
    v1.fixed_in = []
    v1.vulnerable_in = []
    v1.created_at = v1.updated_at = datetime.datetime.utcnow()

    vuln1 = VulnerableArtifact()
    vuln1.created_at = vuln1.updated_at = v1.created_at
    vuln1.namespace_name = v1.namespace_name
    vuln1.name = python_pkg1_100.name
    vuln1.vulnerability_id = v1.id
    vuln1.parent = v1
    vuln1.version = python_pkg1_100.version
    vuln1.include_previous_versions = True
    vuln1.epochless_version = vuln1.version
    vuln1.version_format = (
        "static"  # Random string, but not in set of ['semver', 'rpm', 'deb', 'apk']
    )

    v1.vulnerable_in.append(vuln1)

    assert v1.vulnerable_in[0].match_and_vulnerable(python_pkg1_100)
    assert not v1.vulnerable_in[0].match_and_vulnerable(python_pkg1_101)


def test_multifix_vulnerability(
    vulnerability_with_multifix,
    vulnerable_semver_pkg1,
    vulnerable_semver_pkg2,
    monkeypatch_distros,
):
    """
    Test matches against multiple semver range fixed artifacts (e.g. like a GHSA record)

    :return:
    """
    f = vulnerability_with_multifix.fixed_in[0]
    f2 = vulnerability_with_multifix.fixed_in[1]
    logger.info("Testing package %s", vulnerable_semver_pkg1)
    logger.info("Testing vuln %s", f)
    assert isinstance(f, FixedArtifact)
    assert f.match_but_not_fixed(vulnerable_semver_pkg1)
    assert not f.match_but_not_fixed(vulnerable_semver_pkg2)

    t = ImagePackageVulnerability()
    t.package = vulnerable_semver_pkg1
    t.vulnerability = vulnerability_with_multifix
    assert t.fixed_artifact() == f
    assert t.fixed_in() == "1.1.2"

    logger.info("Testing package %s", vulnerable_semver_pkg2)
    logger.info("Testing vuln %s", f2)
    assert isinstance(f2, FixedArtifact)
    assert not f2.match_but_not_fixed(vulnerable_semver_pkg1)
    assert f2.match_but_not_fixed(vulnerable_semver_pkg2)

    t = ImagePackageVulnerability()
    t.package = vulnerable_semver_pkg2
    t.vulnerability = vulnerability_with_multifix
    assert t.fixed_artifact() == f2
    assert t.fixed_in() == "2.2.2"

    # Unset the fix version
    f2.fix_metadata = {}
    logger.info("Testing vuln with fix removed %s", f2)
    assert isinstance(f2, FixedArtifact)
    assert not f2.match_but_not_fixed(vulnerable_semver_pkg1)
    assert f2.match_but_not_fixed(vulnerable_semver_pkg2)

    t = ImagePackageVulnerability()
    t.package = vulnerable_semver_pkg2
    t.vulnerability = vulnerability_with_multifix
    assert t.fixed_artifact() == f2
    assert t.fixed_in() is None
