import json
import os
from dataclasses import dataclass
from os import path
from typing import Callable, Dict, Generator, Sequence

import pytest

import tests.functional.services.catalog.utils.api as catalog_api
import tests.functional.services.policy_engine.utils.api as policy_engine_api
from anchore_engine.db import session_scope
from anchore_engine.db.entities.catalog import CatalogImage, CatalogImageDocker
from anchore_engine.db.entities.policy_engine import (
    CachedPolicyEvaluation,
    CpeV2Vulnerability,
    FeedMetadata,
    FixedArtifact,
    ImageVulnerabilitiesReport,
    NvdV2Metadata,
    Vulnerability,
)
from tests.functional.services.catalog.utils.utils import add_or_replace_document
from tests.functional.services.policy_engine.conftest import is_legacy_provider
from tests.functional.services.utils import http_utils

CURRENT_DIR = path.dirname(path.abspath(__file__))
ANALYSIS_FILES_DIR = path.join(CURRENT_DIR, "analysis_files")
SEED_FILE_DIR = path.join(CURRENT_DIR, "database_seed_files")
FEEDS_DATA_PATH_PREFIX = path.join("data", "v1", "service", "feeds")
EXPECTED_CONTENT = path.join(CURRENT_DIR, "expected_output")


@dataclass
class AnalysisFile:
    filename: str
    image_digest: str


ANALYSIS_FILES: Sequence[AnalysisFile] = [
    AnalysisFile(
        "alpine-test.json",
        "sha256:80a31c3ce2e99c3691c27ac3b1753163214494e9b2ca07bfdccf29a5cca2bfbe",
    ),
    AnalysisFile(
        "debian-test.json",
        "sha256:406413437f26223183d133ccc7186f24c827729e1b21adc7330dd43fcdc030b3",
    ),
    AnalysisFile(
        "centos-test.json",
        "sha256:fe3ca35038008b0eac0fa4e686bd072c9430000ab7d7853001bde5f5b8ccf60c",
    ),
]

IMAGE_DIGEST_ID_MAP: Dict[str, str] = {}
IMAGE_DIGEST_MAP = {
    "sha256:80a31c3ce2e99c3691c27ac3b1753163214494e9b2ca07bfdccf29a5cca2bfbe": {
        "tag": "anchore/test_images:vulnerabilities-alpine-f5e8952",
        "image_id": "8d4db62fbc412dd3a19f55bdf3d15bed65a7cdf9a3cf00630da685af565e2d25",
    },
    "sha256:406413437f26223183d133ccc7186f24c827729e1b21adc7330dd43fcdc030b3": {
        "tag": "anchore/test_images:vulnerabilities-debian-f5e8952",
        "image_id": "cbe22359b63443e715091e24efbcdeaa6bb3fb96c6fffeb5a1e85caaffc83565",
    },
    "sha256:fe3ca35038008b0eac0fa4e686bd072c9430000ab7d7853001bde5f5b8ccf60c": {
        "tag": "anchore/test_images:vulnerabilities-centos-f5e8952",
        "image_id": "08b3583ff5e85fb755be57d2ae9b14e3b5e4d406a5de55246b2ca84b2035f5da",
    },
}


@pytest.fixture(scope="package")
def add_catalog_documents(request) -> None:
    """
    Adds analyzer manifests to catalog. Deletes existing manifests and images if they exist.
    """
    # Do not load up any catalog documents if legacy test
    if not is_legacy_provider():
        return

    for analysis_file in ANALYSIS_FILES:
        file_path = path.join(ANALYSIS_FILES_DIR, analysis_file.filename)
        with open(file_path, "r") as f:
            file_contents = f.read()
            analysis_document = json.loads(file_contents)
            add_or_replace_document(
                "analysis_data", analysis_file.image_digest, analysis_document
            )
            image_id = analysis_document["document"][0]["image"]["imageId"]
            try:
                policy_engine_api.users.delete_image(image_id)
            except http_utils.RequestFailedError as err:
                if err.status_code != 404:
                    raise err
            IMAGE_DIGEST_ID_MAP[analysis_file.image_digest] = image_id

    def remove_documents_and_image() -> None:
        """
        Cleanup, deletes added images and analyzer manifests.
        """
        for analysis_file in ANALYSIS_FILES:
            catalog_api.objects.delete_document(
                "analysis_data", analysis_file.image_digest
            )
            policy_engine_api.users.delete_image(
                IMAGE_DIGEST_ID_MAP[analysis_file.image_digest]
            )

    request.addfinalizer(remove_documents_and_image)


@pytest.fixture(scope="package")
def ingress_image(add_catalog_documents) -> Callable[[str], http_utils.APIResponse]:
    """
    Returns method that adds new image to policy engine for vulnerability scanning. Moved to fixture to reduce code duplication.
    :return: METHOD that calls ingress_image for the policy engine API with the appropriate catalog URL
    :rtype: Callable[[str], http_utils.APIResponse]
    """

    def _ingress_image(image_digest: str) -> http_utils.APIResponse:
        """
        Adds new image to policy engine for vulnerability scanning. Moved to fixture to reduce code duplication.
        :param image_digest: image digest of image to ingress
        :type image_digest: str
        :return: api response
        :rtype: http_utils.APIResponse
        """
        fetch_url = f"catalog://{http_utils.DEFAULT_API_CONF['ANCHORE_API_ACCOUNT']}/analysis_data/{image_digest}"
        image_id = IMAGE_DIGEST_ID_MAP[image_digest]
        return policy_engine_api.images.ingress_image(fetch_url, image_id)

    return _ingress_image


@pytest.fixture(scope="package")
def ingress_all_images(ingress_image) -> None:
    """
    Ingress all test images.
    """
    for analysis_file in ANALYSIS_FILES:
        ingress_image(analysis_file.image_digest)


@pytest.fixture(scope="session")
def image_digest_id_map() -> Dict[str, str]:
    """
    :return: lookup mapping of image_digest to image_id
    :rtype: Dict[str, str]
    """
    return IMAGE_DIGEST_ID_MAP


SEED_FILE_TO_DB_TABLE_MAP: Dict[str, Callable] = {
    "feed_data_vulnerabilities.json": Vulnerability,
    "feed_data_vulnerabilities_fixed_artifacts.json": FixedArtifact,
    "feed_data_nvdv2_vulnerabilities.json": NvdV2Metadata,
    "feed_data_cpev2_vulnerabilities.json": CpeV2Vulnerability,
    "feeds.json": FeedMetadata,
    "catalog_image.json": CatalogImage,
    "catalog_image_docker.json": CatalogImageDocker,
}

CATALOG_FILES = ["catalog_image.json", "catalog_image_docker.json"]
VULN_DATA_FILES = [
    "feed_data_vulnerabilities.json",
    "feed_data_vulnerabilities_fixed_artifacts.json",
    "feed_data_nvdv2_vulnerabilities.json",
    "feed_data_cpev2_vulnerabilities.json",
    "feeds.json",
]

SEED_FILE_TO_METADATA_MAP: Dict[str, str] = {
    "feed_data_vulnerabilities.json": "metadata_json",
    "feed_data_vulnerabilities_fixed_artifacts.json": "fix_metadata",
}


def load_seed_file_rows(file_name: str) -> Generator[Dict, None, None]:
    """
    Loads database seed files (json lines) and yields the json objects.
    :param file_name: name of seed file to load
    :type file_name: str
    :return: generator yields json
    :rtype: Generator[Dict, None, None]
    """
    json_file = os.path.join(SEED_FILE_DIR, file_name)
    with open(json_file, "rb") as f:
        for line in f:
            linetext = line.decode("unicode_escape").strip()
            json_content = json.loads(linetext)
            if file_name in SEED_FILE_TO_METADATA_MAP:
                json_key = SEED_FILE_TO_METADATA_MAP[file_name]
                if json_content[json_key] is not None:
                    json_content[json_key] = json.loads(json_content[json_key])
            yield json_content


def _setup_vuln_data():
    """
    Loads database seed files and bulk saves all records direclty to db
    """
    with session_scope() as db:
        all_records = []

        files_to_seed = []
        if is_legacy_provider():
            files_to_seed += CATALOG_FILES

        # If legacy provider, add vuln data to be seeded to files
        # if grype provider, ensure the grypedb is synced
        if is_legacy_provider():
            files_to_seed += VULN_DATA_FILES
        else:
            policy_engine_api.feeds.feeds_sync(force_flush=True)

        # seed data to engine db
        for seed_file_name in files_to_seed:
            entry_cls = SEED_FILE_TO_DB_TABLE_MAP[seed_file_name]
            for db_entry in load_seed_file_rows(seed_file_name):
                all_records.append(entry_cls(**db_entry))
        db.bulk_save_objects(all_records)
        db.flush()


@pytest.fixture(scope="package", autouse=True)
def setup_vuln_data(
    request, set_env_vars, anchore_db, teardown_and_recreate_tables
) -> None:
    """
    Writes database seed file content to database. This allows us to ensure consistent vulnerability results (regardless of feed sync status).
    """
    tablenames = [cls.__tablename__ for cls in SEED_FILE_TO_DB_TABLE_MAP.values()]
    tablenames.extend(
        [CachedPolicyEvaluation.__tablename__, ImageVulnerabilitiesReport.__tablename__]
    )
    teardown_and_recreate_tables(tablenames)
    _setup_vuln_data()
    request.addfinalizer(lambda: teardown_and_recreate_tables(tablenames))


@pytest.fixture(scope="package")
def setup_image(ingress_image, add_image_with_teardown_package_scope):
    """
    This fixture is used to get the image being tested into the correct analyzed state to test vulnerabilities
    If its the legacy provider, uses the old method of ingressing image directly into the policy engine
    If grype, uses the api to add and analyze the image
    """

    def _setup_image(image_digest):
        if is_legacy_provider():
            return ingress_image(image_digest)
        else:
            return add_image_with_teardown_package_scope(
                IMAGE_DIGEST_MAP[image_digest]["tag"]
            )

    return _setup_image


@pytest.fixture(scope="package")
def setup_all_images(ingress_image, setup_image):
    if is_legacy_provider():
        for analysis_file in ANALYSIS_FILES:
            ingress_image(analysis_file.image_digest)
    else:
        for tag in IMAGE_DIGEST_MAP.keys():
            setup_image(tag)
