"""Checks that dfiq_test_data.zip's yaml files are internally consistent.

Every object's uuid must be well-formed, and every parent_ids entry must
resolve to another object's uuid or dfiq_id present in the same archive.
DFIQ import calls update_parents(soft_fail=True), so a mismatch here would
otherwise silently produce an unlinked scenario/facet/question hierarchy
instead of a loud failure.

The archive must also only contain yaml files (directory entries aside) -
no scripts, executables, or any other file type. This is checked both by
extension and by sniffing the actual file content with Magika, so a
disguised executable renamed to *.yaml is also rejected.
"""

import io
import tempfile
import unittest
import uuid
from zipfile import ZipFile

import yaml
from magika import Magika

_MAGIKA = Magika()


def validate(archive_path: str) -> list[str]:
    """Checks a DFIQ archive for uuid and parent_ids consistency.

    Args:
        archive_path: path to the DFIQ zip archive to validate.

    Returns:
        A list of human-readable error messages. Empty if the archive is
        consistent.
    """
    errors = []
    yaml_by_uuid = {}

    with ZipFile(archive_path) as archive:
        for name in archive.namelist():
            if name.endswith("/"):
                continue  # directory entry
            if not name.endswith(".yaml"):
                errors.append(f"{name} is not a .yaml file")
                continue

            with archive.open(name) as f:
                content = f.read()

            content_type = _MAGIKA.identify_bytes(content).output.label
            if content_type not in ("yaml", "empty"):
                errors.append(
                    f"{name} has a .yaml extension but its content was "
                    f"detected as {content_type!r}"
                )
                continue

            yaml_data = yaml.safe_load(content)

            if yaml_data.get("uuid") is None:
                errors.append(f"{name} is missing a uuid")
                continue
            try:
                uuid.UUID(str(yaml_data["uuid"]))
            except ValueError:
                errors.append(f"{name} has an invalid uuid: {yaml_data['uuid']!r}")
                continue
            yaml_by_uuid[yaml_data["uuid"]] = (name, yaml_data)

    dfiq_ids = {data["id"] for _, data in yaml_by_uuid.values()}
    for name, yaml_data in yaml_by_uuid.values():
        for parent_id in yaml_data.get("parent_ids") or []:
            if parent_id in yaml_by_uuid or parent_id in dfiq_ids:
                continue
            errors.append(
                f"{name}: parent_id {parent_id!r} referenced by "
                f"{yaml_data['id']} is not present in the archive"
            )

    return errors


class ValidateDFIQArchiveTest(unittest.TestCase):
    def test_committed_archive_is_consistent(self):
        # tests/dfiq_test_data/dfiq_test_data.zip is regenerated by hand
        # whenever its yaml fixtures change (e.g. GHSA-6xf2-8436-rgvf); this
        # guards against its uuids/parent_ids silently drifting out of sync,
        # which would otherwise make DFIQ import skip linking the
        # scenario/facet/question hierarchy (update_parents(soft_fail=True)).
        errors = validate("tests/dfiq_test_data/dfiq_test_data.zip")
        self.assertEqual(errors, [])

    def test_rejects_non_yaml_files(self):
        buffer = io.BytesIO()
        with ZipFile(buffer, "w") as archive:
            archive.writestr(
                "S1003.yaml",
                "type: scenario\nid: S1003\nuuid: "
                "00000000-0000-4000-8000-000000000001\n",
            )
            archive.writestr("setup.sh", "#!/bin/sh\necho pwned\n")

        with tempfile.NamedTemporaryFile(suffix=".zip") as archive_file:
            archive_file.write(buffer.getvalue())
            archive_file.flush()
            errors = validate(archive_file.name)

        self.assertIn("setup.sh is not a .yaml file", errors)

    def test_rejects_disguised_executable(self):
        buffer = io.BytesIO()
        with ZipFile(buffer, "w") as archive:
            archive.writestr(
                "S1003.yaml",
                "type: scenario\nid: S1003\nuuid: "
                "00000000-0000-4000-8000-000000000001\n",
            )
            # A shell script renamed to *.yaml to slip past an extension-only check.
            archive.writestr("payload.yaml", "#!/bin/sh\nrm -rf /\n")

        with tempfile.NamedTemporaryFile(suffix=".zip") as archive_file:
            archive_file.write(buffer.getvalue())
            archive_file.flush()
            errors = validate(archive_file.name)

        self.assertTrue(
            any("payload.yaml" in error and "detected as" in error for error in errors)
        )
