import json
from pathlib import Path
from typing import Any

import pytest
from eip712.convert.input_to_resolved import EIP712InputToResolvedConverter
from eip712.model.input.descriptor import InputEIP712DAppDescriptor

from erc7730.common.pydantic import model_to_json_str
from erc7730.convert.convert import convert_and_print_errors
from erc7730.convert.ledger.eip712.convert_eip712_to_erc7730 import EIP712toERC7730Converter
from erc7730.convert.ledger.eip712.convert_erc7730_to_eip712 import ERC7730toEIP712Converter
from erc7730.convert.resolved.convert_erc7730_input_to_resolved import ERC7730InputToResolved
from erc7730.model.display import FieldFormat
from erc7730.model.input.descriptor import InputERC7730Descriptor
from tests.assertions import assert_dict_equals
from tests.cases import path_id
from tests.dict_utils import del_by_path, is_in_path, map_by_path
from tests.files import ERC7730_EIP712_DESCRIPTORS, LEGACY_EIP712_DESCRIPTORS
from tests.skip import single_or_skip


@pytest.mark.parametrize("input_file", ERC7730_EIP712_DESCRIPTORS, ids=path_id)
def test_roundtrip_from_erc7730(input_file: Path) -> None:
    """
    Test roundtrip conversion ERC-7730 => Ledger legacy EIP-712 => ERC-7730.

    Note the test only applies to descriptors with a single contract and message.
    """
    input_erc7730_descriptor = InputERC7730Descriptor.load(input_file)
    resolved_erc7730_descriptor = convert_and_print_errors(input_erc7730_descriptor, ERC7730InputToResolved())
    resolved_erc7730_descriptor = single_or_skip(resolved_erc7730_descriptor)
    legacy_eip712_descriptor = convert_and_print_errors(resolved_erc7730_descriptor, ERC7730toEIP712Converter())
    legacy_eip712_descriptor = single_or_skip(legacy_eip712_descriptor)
    resolved_legacy_eip172_descriptor = EIP712InputToResolvedConverter().convert(legacy_eip712_descriptor)
    output_erc7730_descriptor = convert_and_print_errors(resolved_legacy_eip172_descriptor, EIP712toERC7730Converter())
    output_erc7730_descriptor = single_or_skip(output_erc7730_descriptor)
    _assert_erc7730_json_equals_with_tolerance(input_erc7730_descriptor, output_erc7730_descriptor)


@pytest.mark.parametrize("input_file", LEGACY_EIP712_DESCRIPTORS, ids=path_id)
def test_roundtrip_from_legacy_eip712(input_file: Path) -> None:
    """
    Test roundtrip conversion Ledger legacy EIP-712 => ERC-7730 => Ledger legacy EIP-712.

    Note the test only applies to descriptors with a single contract and message.
    """

    if str(input_file).endswith("/uniswap/eip712.json"):
        pytest.skip("Several Uniswap messages have same primary types so they collide")

    input_legacy_eip712_descriptor = InputEIP712DAppDescriptor.load(input_file)
    resolved_legacy_eip172_descriptor = EIP712InputToResolvedConverter().convert(input_legacy_eip712_descriptor)
    input_erc7730_descriptor = convert_and_print_errors(resolved_legacy_eip172_descriptor, EIP712toERC7730Converter())
    input_erc7730_descriptor = single_or_skip(input_erc7730_descriptor)
    resolved_erc7730_descriptor = convert_and_print_errors(input_erc7730_descriptor, ERC7730InputToResolved())
    resolved_erc7730_descriptor = single_or_skip(resolved_erc7730_descriptor)
    output_legacy_eip712_descriptor = convert_and_print_errors(resolved_erc7730_descriptor, ERC7730toEIP712Converter())
    output_legacy_eip712_descriptor = single_or_skip(output_legacy_eip712_descriptor)
    _assert_eip712_json_equals_with_tolerance(input_legacy_eip712_descriptor, output_legacy_eip712_descriptor)


def _assert_erc7730_json_equals_with_tolerance(input: InputERC7730Descriptor, output: InputERC7730Descriptor) -> None:
    input_dict, output_dict = json.loads(model_to_json_str(input)), json.loads(model_to_json_str(output))

    # $schema is not present in EIP-712
    del_by_path(input_dict, "$schema")

    # Addresses may be in EIP-55 format, convert to lower case in input
    def _lowercase_addresses(deployments: list[dict[str, Any]]) -> Any:
        for deployment in deployments:
            if "address" in deployment:
                deployment["address"] = deployment["address"].lower()
        return deployments

    map_by_path(
        input_dict,
        _lowercase_addresses,
        "context",
        "eip712",
        "deployments",
    )

    # chainId and verifyingContract are always generated by the converter
    # even when not explicitly present in the input
    for key in "chainId", "verifyingContract":
        path = "context", "eip712", "domain", key
        if not is_in_path(input_dict, *path):
            del_by_path(output_dict, *path)

    # Some metadata cannot be stored in EIP712, and thus are lost with the roundtrip
    del_by_path(input_dict, "metadata", "info")
    del_by_path(input_dict, "metadata", "token")
    del_by_path(input_dict, "metadata", "token")
    del_by_path(input_dict, "context", "eip712", "domain", "version")

    # Adapt formats objects to match EIP-712 conversion
    def _cleanup_formats(formats: dict[str, Any]) -> Any:
        for _, message in formats.items():
            # Remove ERC-7730 specific fields
            del_by_path(message, "$id")
            del_by_path(message, "required")
            del_by_path(message, "excluded")
            del_by_path(message, "screens")
            if "fields" in message:
                for field in message["fields"]:
                    if "format" in field:
                        format = field["format"]

                        # Address name format parameters cannot be preserved
                        if format == "addressName":
                            del_by_path(field, "params")

                        # Other formats are always converted to RAW
                        if format not in (
                            FieldFormat.AMOUNT,
                            FieldFormat.TOKEN_AMOUNT,
                            FieldFormat.DATE,
                        ):
                            field["format"] = "raw"

        return formats

    map_by_path(input_dict, _cleanup_formats, "display", "formats")

    assert_dict_equals(input_dict, output_dict)


def _assert_eip712_json_equals_with_tolerance(
    input: InputEIP712DAppDescriptor, output: InputEIP712DAppDescriptor
) -> None:
    input_dict, output_dict = json.loads(model_to_json_str(input)), json.loads(model_to_json_str(output))

    # format is required in ERC-7730 schema, and missing format in EIP-712 is equivalent to raw
    def cleanup_dapp_descriptor(dapp: dict[str, Any]) -> Any:
        for contract in dapp["contracts"]:
            for message in contract["messages"]:
                for field in message["mapper"]["fields"]:
                    if field.get("format") == "raw":
                        field.pop("format", None)

    cleanup_dapp_descriptor(input_dict)
    cleanup_dapp_descriptor(output_dict)

    assert_dict_equals(input_dict, output_dict)
