# Copyright New York University and the TUF contributors
# SPDX-License-Identifier: MIT OR Apache-2.0

"""Test __eq__ implementations of classes inside tuf/api/metadata.py."""

from __future__ import annotations

import copy
import os
import sys
import unittest
from typing import Any, ClassVar

from securesystemslib.signer import SSlibKey

from tests import utils
from tuf.api.metadata import (
    TOP_LEVEL_ROLE_NAMES,
    DelegatedRole,
    Delegations,
    Metadata,
    MetaFile,
    Role,
    Signature,
    SuccinctRoles,
    TargetFile,
)


class TestMetadataComparisons(unittest.TestCase):
    """Test __eq__ for all classes inside tuf/api/metadata.py."""

    metadata: ClassVar[dict[str, bytes]]

    @classmethod
    def setUpClass(cls) -> None:
        cls.repo_dir = os.path.join(
            utils.TESTS_DIR, "repository_data", "repository", "metadata"
        )

        # Store class instances in this dict instead of creating them inside the
        # test function in order to escape the need for reinitialization of the
        # instances on each run of the test function.
        cls.objects = {}
        for md in TOP_LEVEL_ROLE_NAMES:
            with open(os.path.join(cls.repo_dir, f"{md}.json"), "rb") as f:
                data = f.read()
                cls.objects[md.capitalize()] = Metadata.from_bytes(data).signed

        cls.objects["Metadata"] = Metadata(cls.objects["Timestamp"], {})
        cls.objects["Signed"] = cls.objects["Timestamp"]
        cls.objects["Key"] = SSlibKey(
            "id", "rsa", "rsassa-pss-sha256", {"public": "foo"}
        )
        cls.objects["Role"] = Role(["keyid1", "keyid2"], 3)
        cls.objects["MetaFile"] = MetaFile(1, 12, {"sha256": "abc"})
        cls.objects["DelegatedRole"] = DelegatedRole("a", [], 1, False, ["d"])
        cls.objects["SuccinctRoles"] = SuccinctRoles(["keyid"], 1, 8, "foo")
        cls.objects["Delegations"] = Delegations(
            {"keyid": cls.objects["Key"]}, {"a": cls.objects["DelegatedRole"]}
        )
        cls.objects["TargetFile"] = TargetFile(
            1, {"sha256": "abc"}, "file1.txt"
        )

    # Keys are class names.
    # Values are dictionaries containing attribute names and their new values.
    classes_attributes_modifications = {
        "Metadata": {"signed": None, "signatures": None},
        "Signed": {"version": -1, "spec_version": "0.0.0"},
        "Key": {"keyid": "a", "keytype": "foo", "scheme": "b", "keyval": "b"},
        "Role": {"keyids": [], "threshold": 10},
        "Root": {"consistent_snapshot": None, "keys": {}},
        "MetaFile": {"version": None, "length": None, "hashes": {}},
        "Timestamp": {"snapshot_meta": None},
        "Snapshot": {"meta": None},
        "DelegatedRole": {
            "name": "",
            "terminating": None,
            "paths": [""],
            "path_hash_prefixes": [""],
        },
        "SuccinctRoles": {"bit_length": 0, "name_prefix": ""},
        "Delegations": {"keys": {}, "roles": {}},
        "TargetFile": {"length": 0, "hashes": {}, "path": ""},
        "Targets": {"targets": {}, "delegations": []},
    }

    @utils.run_sub_tests_with_dataset(classes_attributes_modifications)
    def test_classes_eq_(self, test_case_data: dict[str, Any]) -> None:
        obj = self.objects[self.case_name]

        # Assert that obj is not equal to an object from another type
        self.assertNotEqual(obj, "")
        obj_2 = copy.deepcopy(obj)
        # Assert that __eq__ works for equal objects.
        self.assertEqual(obj, obj_2)

        for attr, value in test_case_data.items():
            original_value = getattr(obj_2, attr)
            setattr(obj_2, attr, value)
            # Assert that the original object != modified one.
            self.assertNotEqual(obj, obj_2, f"Failed case: {attr}")
            # Restore the old value of the attribute.
            setattr(obj_2, attr, original_value)

    def test_md_eq_signatures_reversed_order(self) -> None:
        # Test comparing objects with same signatures but different order.

        # Remove all signatures and create new ones.
        md: Metadata = self.objects["Metadata"]
        md.signatures = {"a": Signature("a", "a"), "b": Signature("b", "b")}
        md_2 = copy.deepcopy(md)
        # Reverse signatures order in md_2.
        md_2.signatures = dict(reversed(md_2.signatures.items()))
        # Assert that both objects are not the same because of signatures order.
        self.assertNotEqual(md, md_2)

        # but if we fix the signatures order they will be equal
        md_2.signatures = {"a": Signature("a", "a"), "b": Signature("b", "b")}
        self.assertEqual(md, md_2)

    def test_md_eq_special_signatures_tests(self) -> None:
        # Test that metadata objects with different signatures are not equal.
        md: Metadata = self.objects["Metadata"]
        md_2 = copy.deepcopy(md)
        md_2.signatures = {}
        self.assertNotEqual(md, md_2)

        # Test that metadata objects with empty signatures are equal
        md.signatures = {}
        self.assertEqual(md, md_2)

        # Metadata objects with different signatures types are not equal.
        md_2.signatures = ""  # type: ignore[assignment]
        self.assertNotEqual(md, md_2)

    def test_delegations_eq_roles_reversed_order(self) -> None:
        # Test comparing objects with same delegated roles but different order.
        role_one_dict = {
            "keyids": ["keyid1"],
            "name": "a",
            "terminating": False,
            "paths": ["fn1"],
            "threshold": 1,
        }
        role_two_dict = {
            "keyids": ["keyid2"],
            "name": "b",
            "terminating": True,
            "paths": ["fn2"],
            "threshold": 4,
        }

        delegations_dict = {
            "keys": {
                "keyid2": {
                    "keytype": "ed25519",
                    "scheme": "ed25519",
                    "keyval": {"public": "bar"},
                }
            },
            "roles": [role_one_dict, role_two_dict],
        }
        delegations = Delegations.from_dict(copy.deepcopy(delegations_dict))

        # Create a second delegations obj with reversed roles order
        delegations_2 = copy.deepcopy(delegations)
        assert isinstance(delegations.roles, dict)
        delegations_2.roles = dict(reversed(delegations.roles.items()))

        # Both objects are not the equal because of delegated roles order.
        self.assertNotEqual(delegations, delegations_2)

        # but if we fix the delegated roles order they will be equal
        delegations_2.roles = delegations.roles

        self.assertEqual(delegations, delegations_2)


# Run unit test.
if __name__ == "__main__":
    utils.configure_test_logging(sys.argv)
    unittest.main()
