# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for structural equality/hashing on py_class-defined types.

Mirrors the C++ tests in tests/cpp/extra/test_structural_equal_hash.cc,
porting the object-level tests (FreeVar, FuncDefAndIgnoreField, etc.)
to Python using ``@py_class(structural_eq=...)`` and ``field(structural_eq=...)``.
"""

from __future__ import annotations

import pytest
import tvm_ffi
from tvm_ffi import get_first_structural_mismatch, structural_equal, structural_hash
from tvm_ffi.dataclasses import field, py_class

# ---------------------------------------------------------------------------
# Type definitions (mirror testing_object.h)
# ---------------------------------------------------------------------------


@py_class("testing.py.Var", structural_eq="var")
class TVar(tvm_ffi.Object):
    """Variable node — compared by binding position, not by name.

    Mirrors C++ TVarObj with:
      _type_s_eq_hash_kind = kTVMFFISEqHashKindFreeVar
      name field has SEqHashIgnore
    """

    name: str = field(structural_eq="ignore")


@py_class("testing.py.Int", structural_eq="tree")
class TInt(tvm_ffi.Object):
    """Simple integer literal node."""

    value: int


@py_class("testing.py.Func", structural_eq="tree")
class TFunc(tvm_ffi.Object):
    """Function node with definition region and ignored comment.

    Mirrors C++ TFuncObj with:
      params has SEqHashDef
      comment has SEqHashIgnore
    """

    params: list = field(structural_eq="def")
    body: list
    comment: str = field(structural_eq="ignore", default="")


@py_class("testing.py.Expr", structural_eq="tree")
class TExpr(tvm_ffi.Object):
    """A simple expression node for tree-comparison tests."""

    value: int


@py_class("testing.py.Metadata", structural_eq="const-tree")
class TMetadata(tvm_ffi.Object):
    """Immutable metadata node — pointer shortcut is safe (no var children)."""

    tag: str
    version: int


@py_class("testing.py.Binding", structural_eq="dag")
class TBinding(tvm_ffi.Object):
    """Binding node — sharing structure is semantically meaningful."""

    name: str
    value: int


# ---------------------------------------------------------------------------
# Tests: FreeVar (mirrors C++ FreeVar test)
# ---------------------------------------------------------------------------


class TestFreeVar:
    """Test structural_eq="var" kind (C++ kTVMFFISEqHashKindFreeVar)."""

    def test_free_var_equal_with_mapping(self) -> None:
        """Two different vars are equal when map_free_vars=True."""
        a = TVar("a")
        b = TVar("b")
        assert structural_equal(a, b, map_free_vars=True)

    def test_free_var_not_equal_without_mapping(self) -> None:
        """Two different vars are NOT equal by default (no mapping)."""
        a = TVar("a")
        b = TVar("b")
        assert not structural_equal(a, b)

    def test_free_var_hash_differs_without_mapping(self) -> None:
        """Without mapping, different vars produce different hashes."""
        a = TVar("a")
        b = TVar("b")
        assert structural_hash(a) != structural_hash(b)

    def test_free_var_hash_equal_with_mapping(self) -> None:
        """With map_free_vars, positional hashing makes them equal."""
        a = TVar("a")
        b = TVar("b")
        assert structural_hash(a, map_free_vars=True) == structural_hash(b, map_free_vars=True)

    def test_free_var_same_pointer(self) -> None:
        """Same variable is always equal to itself."""
        x = TVar("x")
        assert structural_equal(x, x)

    def test_free_var_name_ignored(self) -> None:
        """The name field is structural_eq="ignore", so it doesn't affect comparison."""
        a = TVar("different_name_a")
        b = TVar("different_name_b")
        # Names differ, but with mapping they are still equal
        assert structural_equal(a, b, map_free_vars=True)


# ---------------------------------------------------------------------------
# Tests: FuncDefAndIgnoreField (mirrors C++ FuncDefAndIgnoreField test)
# ---------------------------------------------------------------------------


class TestFuncDefAndIgnore:
    """Test structural_eq="def" and structural_eq="ignore" on fields."""

    def test_alpha_equivalent_functions(self) -> None:
        """fun(x){1, x} with comment_a == fun(y){1, y} with comment_b."""
        x = TVar("x")
        y = TVar("y")
        fa = TFunc(params=[x], body=[TInt(1), x], comment="comment a")
        fb = TFunc(params=[y], body=[TInt(1), y], comment="comment b")
        assert structural_equal(fa, fb)
        assert structural_hash(fa) == structural_hash(fb)

    def test_different_body(self) -> None:
        """fun(x){1, x} != fun(x){1, 2} — body differs at index 1."""
        x = TVar("x")
        fa = TFunc(params=[x], body=[TInt(1), x], comment="comment a")
        fc = TFunc(params=[x], body=[TInt(1), TInt(2)], comment="comment c")
        assert not structural_equal(fa, fc)

    def test_mismatch_path(self) -> None:
        """GetFirstMismatch reports the correct access path."""
        x = TVar("x")
        fa = TFunc(params=[x], body=[TInt(1), x])
        fc = TFunc(params=[x], body=[TInt(1), TInt(2)])
        mismatch = get_first_structural_mismatch(fa, fc)
        assert mismatch is not None

    def test_comment_ignored(self) -> None:
        """Identical structure with different comments are equal."""
        x = TVar("x")
        f1 = TFunc(params=[x], body=[TInt(1)], comment="first")
        f2 = TFunc(params=[x], body=[TInt(1)], comment="second")
        assert structural_equal(f1, f2)
        assert structural_hash(f1) == structural_hash(f2)

    def test_inconsistent_var_usage(self) -> None:
        """fun(x,y){x+x} != fun(a,b){a+b} — inconsistent variable mapping."""
        x, y = TVar("x"), TVar("y")
        a, b = TVar("a"), TVar("b")
        f1 = TFunc(params=[x, y], body=[x, x])
        f2 = TFunc(params=[a, b], body=[a, b])
        assert not structural_equal(f1, f2)

    def test_multi_param_alpha_equiv(self) -> None:
        """fun(x,y){x, y} == fun(a,b){a, b} — consistent variable renaming."""
        x, y = TVar("x"), TVar("y")
        a, b = TVar("a"), TVar("b")
        f1 = TFunc(params=[x, y], body=[x, y])
        f2 = TFunc(params=[a, b], body=[a, b])
        assert structural_equal(f1, f2)
        assert structural_hash(f1) == structural_hash(f2)

    def test_swapped_params_not_equal(self) -> None:
        """fun(x,y){x, y} != fun(a,b){b, a} — reversed usage."""
        x, y = TVar("x"), TVar("y")
        a, b = TVar("a"), TVar("b")
        f1 = TFunc(params=[x, y], body=[x, y])
        f2 = TFunc(params=[a, b], body=[b, a])
        assert not structural_equal(f1, f2)

    def test_nested_functions(self) -> None:
        """Nested function with inner binding — alpha-equivalence is scoped."""
        x = TVar("x")
        y = TVar("y")
        inner_x = TFunc(params=[x], body=[x])
        inner_y = TFunc(params=[y], body=[y])
        outer_a = TFunc(params=[], body=[inner_x])
        outer_b = TFunc(params=[], body=[inner_y])
        assert structural_equal(outer_a, outer_b)
        assert structural_hash(outer_a) == structural_hash(outer_b)


# ---------------------------------------------------------------------------
# Tests: tree kind basics
# ---------------------------------------------------------------------------


class TestTreeNode:
    """Test structural_eq="tree" kind."""

    def test_equal_content(self) -> None:
        """Two tree nodes with identical content are structurally equal."""
        a = TExpr(value=42)
        b = TExpr(value=42)
        assert structural_equal(a, b)
        assert structural_hash(a) == structural_hash(b)

    def test_different_content(self) -> None:
        """Two tree nodes with different content are not equal."""
        a = TExpr(value=1)
        b = TExpr(value=2)
        assert not structural_equal(a, b)
        assert structural_hash(a) != structural_hash(b)

    def test_sharing_invisible(self) -> None:
        """Under "tree", sharing doesn't affect equality."""
        s = TExpr(value=10)
        # Two arrays referencing the same object vs two copies
        shared = tvm_ffi.Array([s, s])
        copies = tvm_ffi.Array([TExpr(value=10), TExpr(value=10)])
        assert structural_equal(shared, copies)
        assert structural_hash(shared) == structural_hash(copies)


# ---------------------------------------------------------------------------
# Tests: const-tree kind
# ---------------------------------------------------------------------------


class TestConstTreeNode:
    """Test structural_eq="const-tree" kind."""

    def test_equal_content(self) -> None:
        """Two const-tree nodes with identical content are structurally equal."""
        a = TMetadata(tag="v1", version=1)
        b = TMetadata(tag="v1", version=1)
        assert structural_equal(a, b)
        assert structural_hash(a) == structural_hash(b)

    def test_different_content(self) -> None:
        """Two const-tree nodes with different content are not equal."""
        a = TMetadata(tag="v1", version=1)
        b = TMetadata(tag="v1", version=2)
        assert not structural_equal(a, b)

    def test_same_pointer_shortcircuits(self) -> None:
        """Same pointer should be equal (the const-tree optimization)."""
        a = TMetadata(tag="test", version=1)
        assert structural_equal(a, a)


# ---------------------------------------------------------------------------
# Tests: dag kind
# ---------------------------------------------------------------------------


class TestDAGNode:
    """Test structural_eq="dag" kind."""

    def test_same_dag_shape(self) -> None:
        """Two DAGs with the same sharing shape are equal."""
        s1 = TBinding(name="s", value=1)
        s2 = TBinding(name="s", value=1)
        dag1 = tvm_ffi.Array([s1, s1])  # shared
        dag2 = tvm_ffi.Array([s2, s2])  # shared (same shape)
        assert structural_equal(dag1, dag2)

    def test_dag_vs_tree_not_equal(self) -> None:
        """A DAG (shared) vs tree (independent copies) are NOT equal."""
        shared = TBinding(name="s", value=1)
        copy1 = TBinding(name="s", value=1)
        copy2 = TBinding(name="s", value=1)
        dag = tvm_ffi.Array([shared, shared])
        tree = tvm_ffi.Array([copy1, copy2])
        assert not structural_equal(dag, tree)

    def test_dag_vs_tree_hash_differs(self) -> None:
        """DAG and tree with same content should hash differently."""
        shared = TBinding(name="s", value=1)
        copy1 = TBinding(name="s", value=1)
        copy2 = TBinding(name="s", value=1)
        dag = tvm_ffi.Array([shared, shared])
        tree = tvm_ffi.Array([copy1, copy2])
        assert structural_hash(dag) != structural_hash(tree)

    def test_reverse_bijection(self) -> None:
        """(a, b) vs (a, a) where a ≅ b — reverse map detects inconsistency."""
        a = TBinding(name="a", value=1)
        b = TBinding(name="b", value=1)  # same content as a
        lhs = tvm_ffi.Array([a, b])
        rhs = tvm_ffi.Array([a, a])  # note: same object twice
        assert not structural_equal(lhs, rhs)


# ---------------------------------------------------------------------------
# Tests: unsupported kind (default)
# ---------------------------------------------------------------------------


class TestUnsupported:
    """Test that types without structural_eq= raise on structural comparison."""

    def test_unsupported_raises_on_hash(self) -> None:
        """Structural hashing raises TypeError for types without structural_eq=."""

        @py_class("testing.py.Plain")
        class Plain(tvm_ffi.Object):
            x: int

        with pytest.raises(TypeError):
            structural_hash(Plain(x=1))

    def test_unsupported_raises_on_equal(self) -> None:
        """Structural equality raises TypeError for types without structural_eq=."""

        @py_class("testing.py.Plain2")
        class Plain2(tvm_ffi.Object):
            x: int

        with pytest.raises(TypeError):
            structural_equal(Plain2(x=1), Plain2(x=1))


# ---------------------------------------------------------------------------
# Tests: validation
# ---------------------------------------------------------------------------


class TestValidation:
    """Test that invalid structural_eq= values are rejected."""

    def test_invalid_type_structure(self) -> None:
        """Invalid type-level structural_eq= value raises ValueError."""
        with pytest.raises(ValueError, match="structural_eq"):
            py_class(structural_eq="invalid")

    def test_invalid_field_structure(self) -> None:
        """Invalid field-level structural_eq= value raises ValueError."""
        with pytest.raises(ValueError, match="structural_eq"):
            field(structural_eq="bad_value")
