"""
Lambda Calculus Tests - Python parity with mcard-js

These tests verify that the Python Lambda Calculus implementation
produces identical results to the JavaScript implementation.
"""

import pytest

from mcard.model.card_collection import CardCollection
from mcard.ptr.lambda_calc import (
    LambdaRuntime,
    ParseError,
    alpha_equivalent,
    alpha_normalize,
    # Alpha Conversion
    alpha_rename,
    # Beta Reduction
    beta_reduce,
    eta_equivalent,
    eta_expand,
    # Eta Conversion
    eta_reduce,
    # Free Variables
    free_variables,
    is_abs,
    is_app,
    is_closed,
    is_redex,
    is_var,
    load_term,
    mk_abs,
    mk_app,
    # Term constructors
    mk_var,
    normalize,
    # Parser and Runtime
    parse_lambda_expression,
    pretty_print_deep,
    # Term operations
    store_term,
)


@pytest.fixture
def collection():
    """Create a fresh in-memory CardCollection for each test"""
    return CardCollection(db_path=":memory:")


# ─────────────────────────────────────────────────────────────────────────────
# Term Construction Tests (Parity with Lambda.test.ts > Term Construction)
# ─────────────────────────────────────────────────────────────────────────────


class TestTermConstruction:
    """Test Lambda term construction and type checking"""

    def test_create_variable_term(self, collection):
        """should create a variable term"""
        var = mk_var("x")
        assert is_var(var)
        assert var.name == "x"
        assert var.tag == "Var"

    def test_create_abstraction_term(self, collection):
        """should create an abstraction term"""
        body_hash = store_term(collection, mk_var("x"))
        abs_term = mk_abs("x", body_hash)
        assert is_abs(abs_term)
        assert abs_term.param == "x"
        assert abs_term.body == body_hash

    def test_create_application_term(self, collection):
        """should create an application term"""
        func_hash = store_term(collection, mk_var("f"))
        arg_hash = store_term(collection, mk_var("x"))
        app = mk_app(func_hash, arg_hash)
        assert is_app(app)
        assert app.func == func_hash
        assert app.arg == arg_hash

    def test_store_and_load_term(self, collection):
        """should store and load terms from collection"""
        original = mk_var("test")
        hash_val = store_term(collection, original)
        loaded = load_term(collection, hash_val)
        assert loaded is not None
        assert is_var(loaded)
        assert loaded.name == "test"


# ─────────────────────────────────────────────────────────────────────────────
# Free Variables Tests (Parity with Lambda.test.ts > Free Variables)
# ─────────────────────────────────────────────────────────────────────────────


class TestFreeVariables:
    """Test free variable analysis"""

    def test_free_variables_of_variable(self, collection):
        """should find free variable in var term"""
        x_hash = store_term(collection, mk_var("x"))
        fv = free_variables(collection, x_hash)
        assert fv == {"x"}

    def test_free_variables_of_abstraction(self, collection):
        """should exclude bound variable from free set"""
        # λx.x - x is bound
        x_hash = store_term(collection, mk_var("x"))
        abs_hash = store_term(collection, mk_abs("x", x_hash))
        fv = free_variables(collection, abs_hash)
        assert fv == set()

    def test_free_variables_of_application(self, collection):
        """should union free variables in application"""
        # f x - both f and x are free
        f_hash = store_term(collection, mk_var("f"))
        x_hash = store_term(collection, mk_var("x"))
        app_hash = store_term(collection, mk_app(f_hash, x_hash))
        fv = free_variables(collection, app_hash)
        assert fv == {"f", "x"}

    def test_is_closed_for_combinator(self, collection):
        """should identify closed terms (combinators)"""
        # I = λx.x (closed)
        x_hash = store_term(collection, mk_var("x"))
        i_hash = store_term(collection, mk_abs("x", x_hash))
        assert is_closed(collection, i_hash) == True

    def test_is_not_closed_for_free_variable(self, collection):
        """should identify open terms"""
        x_hash = store_term(collection, mk_var("x"))
        assert is_closed(collection, x_hash) == False


# ─────────────────────────────────────────────────────────────────────────────
# Alpha Conversion Tests (Parity with Lambda.test.ts > Alpha Conversion)
# ─────────────────────────────────────────────────────────────────────────────


class TestAlphaConversion:
    """Test alpha conversion (variable renaming)"""

    def test_alpha_rename_bound_variable(self, collection):
        """should rename bound variable"""
        # λx.x -> λy.y
        x_hash = store_term(collection, mk_var("x"))
        abs_hash = store_term(collection, mk_abs("x", x_hash))

        renamed = alpha_rename(collection, abs_hash, "x", "y")
        loaded = load_term(collection, renamed)
        assert is_abs(loaded)
        assert loaded.param == "y"

    def test_alpha_equivalent_identity(self, collection):
        """should identify alpha-equivalent terms"""
        # λx.x ≡α λy.y
        x_hash = store_term(collection, mk_var("x"))
        abs_x = store_term(collection, mk_abs("x", x_hash))

        y_hash = store_term(collection, mk_var("y"))
        abs_y = store_term(collection, mk_abs("y", y_hash))

        assert alpha_equivalent(collection, abs_x, abs_y) == True

    def test_alpha_not_equivalent_different_structure(self, collection):
        """should distinguish structurally different terms"""
        x_hash = store_term(collection, mk_var("x"))
        y_hash = store_term(collection, mk_var("y"))

        assert alpha_equivalent(collection, x_hash, y_hash) == False

    def test_alpha_normalize_produces_canonical_form(self, collection):
        """should produce canonical naming"""
        # λx.λy.x y -> λx0.λx1.x0 x1
        x_hash = store_term(collection, mk_var("x"))
        y_hash = store_term(collection, mk_var("y"))
        app_hash = store_term(collection, mk_app(x_hash, y_hash))
        abs_y_hash = store_term(collection, mk_abs("y", app_hash))
        abs_x_hash = store_term(collection, mk_abs("x", abs_y_hash))

        normalized = alpha_normalize(collection, abs_x_hash)
        pretty = pretty_print_deep(collection, normalized)

        # Should have x0 and x1 as variable names
        assert "x0" in pretty
        assert "x1" in pretty


# ─────────────────────────────────────────────────────────────────────────────
# Beta Reduction Tests (Parity with Lambda.test.ts > Beta Reduction)
# ─────────────────────────────────────────────────────────────────────────────


class TestBetaReduction:
    """Test beta reduction"""

    def test_is_redex_positive(self, collection):
        """should identify a redex"""
        # (λx.x) y - is a redex
        x_hash = store_term(collection, mk_var("x"))
        abs_hash = store_term(collection, mk_abs("x", x_hash))
        y_hash = store_term(collection, mk_var("y"))
        app_hash = store_term(collection, mk_app(abs_hash, y_hash))

        assert is_redex(collection, app_hash) == True

    def test_is_redex_negative(self, collection):
        """should identify non-redex"""
        # x y - not a redex
        x_hash = store_term(collection, mk_var("x"))
        y_hash = store_term(collection, mk_var("y"))
        app_hash = store_term(collection, mk_app(x_hash, y_hash))

        assert is_redex(collection, app_hash) == False

    def test_beta_reduce_identity_application(self, collection):
        """should reduce (λx.x) y → y"""
        # (λx.x) y -> y
        x_hash = store_term(collection, mk_var("x"))
        abs_hash = store_term(collection, mk_abs("x", x_hash))
        y_hash = store_term(collection, mk_var("y"))
        app_hash = store_term(collection, mk_app(abs_hash, y_hash))

        reduced = beta_reduce(collection, app_hash)
        pretty = pretty_print_deep(collection, reduced)

        assert pretty == "y"

    def test_normalize_identity_application(self, collection):
        """should normalize (λx.x) y to y"""
        x_hash = store_term(collection, mk_var("x"))
        abs_hash = store_term(collection, mk_abs("x", x_hash))
        y_hash = store_term(collection, mk_var("y"))
        app_hash = store_term(collection, mk_app(abs_hash, y_hash))

        result = normalize(collection, app_hash)
        assert result is not None
        assert result.steps == 1
        assert pretty_print_deep(collection, result.normal_form) == "y"

    def test_normalize_k_combinator(self, collection):
        """should normalize K combinator application"""
        # (λx.λy.x) a b → a
        x_hash = store_term(collection, mk_var("x"))
        inner_abs = store_term(collection, mk_abs("y", x_hash))
        k_hash = store_term(collection, mk_abs("x", inner_abs))

        a_hash = store_term(collection, mk_var("a"))
        b_hash = store_term(collection, mk_var("b"))

        app1 = store_term(collection, mk_app(k_hash, a_hash))
        app2 = store_term(collection, mk_app(app1, b_hash))

        result = normalize(collection, app2)
        assert result is not None
        assert pretty_print_deep(collection, result.normal_form) == "a"

    def test_capture_avoidance_in_substitution(self, collection):
        """should avoid variable capture during substitution"""
        # (λy.λx.y) x should NOT incorrectly capture x
        y_hash = store_term(collection, mk_var("y"))
        inner_abs = store_term(collection, mk_abs("x", y_hash))
        outer_abs = store_term(collection, mk_abs("y", inner_abs))

        x_arg = store_term(collection, mk_var("x"))
        app_hash = store_term(collection, mk_app(outer_abs, x_arg))

        result = normalize(collection, app_hash)
        assert result is not None
        # Result should be λx'.x (x is free, bound x renamed)
        normalized = load_term(collection, result.normal_form)
        assert is_abs(normalized)
        # The param should NOT be 'x' to avoid capture
        # (or if it is 'x', the body should refer to free x)


# ─────────────────────────────────────────────────────────────────────────────
# Eta Conversion Tests (Parity with Lambda.test.ts > Eta Conversion)
# ─────────────────────────────────────────────────────────────────────────────


class TestEtaConversion:
    """Test eta conversion"""

    def test_eta_reduce_valid_redex(self, collection):
        """should eta-reduce λx.(M x) → M when x not free in M"""
        # λx.(f x) → f (when x not in f)
        f_hash = store_term(collection, mk_var("f"))
        x_hash = store_term(collection, mk_var("x"))
        app_hash = store_term(collection, mk_app(f_hash, x_hash))
        abs_hash = store_term(collection, mk_abs("x", app_hash))

        reduced = eta_reduce(collection, abs_hash)
        pretty = pretty_print_deep(collection, reduced)

        assert pretty == "f"

    def test_eta_expand(self, collection):
        """should eta-expand M → λx.(M x)"""
        f_hash = store_term(collection, mk_var("f"))

        expanded = eta_expand(collection, f_hash)
        loaded = load_term(collection, expanded)

        assert is_abs(loaded)

    def test_eta_equivalent(self, collection):
        """should identify eta-equivalent terms"""
        # f ≡η λx.(f x)
        f_hash = store_term(collection, mk_var("f"))
        expanded = eta_expand(collection, f_hash)

        assert eta_equivalent(collection, f_hash, expanded) == True


# ─────────────────────────────────────────────────────────────────────────────
# Parser Tests (Parity with Lambda.test.ts > Parser)
# ─────────────────────────────────────────────────────────────────────────────


class TestParser:
    """Test Lambda expression parser"""

    def test_parse_variable(self, collection):
        """should parse a variable"""
        x_hash = parse_lambda_expression(collection, "x")
        term = load_term(collection, x_hash)
        assert is_var(term)
        assert term.name == "x"

    def test_parse_abstraction_backslash(self, collection):
        """should parse abstraction with backslash"""
        abs_hash = parse_lambda_expression(collection, r"\x.x")
        pretty = pretty_print_deep(collection, abs_hash)
        assert pretty == "(λx.x)"

    def test_parse_abstraction_lambda(self, collection):
        """should parse abstraction with λ"""
        abs_hash = parse_lambda_expression(collection, "λx.x")
        pretty = pretty_print_deep(collection, abs_hash)
        assert pretty == "(λx.x)"

    def test_parse_application(self, collection):
        """should parse application"""
        app_hash = parse_lambda_expression(collection, "f x")
        pretty = pretty_print_deep(collection, app_hash)
        assert pretty == "(f x)"

    def test_parse_nested_application(self, collection):
        """should parse nested application (left-associative)"""
        app_hash = parse_lambda_expression(collection, "f x y")
        pretty = pretty_print_deep(collection, app_hash)
        assert pretty == "((f x) y)"

    def test_parse_parenthesized_expression(self, collection):
        """should parse parenthesized expression"""
        app_hash = parse_lambda_expression(collection, r"(\x.x) y")
        result = normalize(collection, app_hash)
        assert result is not None
        assert pretty_print_deep(collection, result.normal_form) == "y"

    def test_parse_complex_expression(self, collection):
        """should parse complex expression"""
        # Church K combinator applied
        expr = r"(\x.\y.x) a b"
        app_hash = parse_lambda_expression(collection, expr)
        result = normalize(collection, app_hash)
        assert result is not None
        assert pretty_print_deep(collection, result.normal_form) == "a"

    def test_parse_error_on_invalid_input(self, collection):
        """should raise ParseError on invalid input"""
        with pytest.raises(ParseError):
            parse_lambda_expression(collection, "()")

    def test_parse_church_numerals(self, collection):
        """should parse Church numerals"""
        # Church 0 = λf.λx.x
        zero = parse_lambda_expression(collection, r"\f.\x.x")
        assert is_closed(collection, zero) == True

        # Church 1 = λf.λx.f x
        one = parse_lambda_expression(collection, r"\f.\x.f x")
        assert is_closed(collection, one) == True

        # Church 2 = λf.λx.f (f x)
        two = parse_lambda_expression(collection, r"\f.\x.f (f x)")
        assert is_closed(collection, two) == True


# ─────────────────────────────────────────────────────────────────────────────
# Lambda Runtime Tests (Parity with Lambda.test.ts > Lambda Runtime)
# ─────────────────────────────────────────────────────────────────────────────


class TestLambdaRuntime:
    """Test LambdaRuntime PTR interface"""

    def test_runtime_parse_operation(self, collection):
        """should execute parse operation"""
        runtime = LambdaRuntime(collection)
        result = runtime.execute(
            "", {"expression": r"\x.x"}, {"operation": "parse"}, ""
        )

        assert result.success
        assert result.term_hash is not None
        assert result.pretty_print == "(λx.x)"

    def test_runtime_normalize_operation(self, collection):
        """should execute normalize operation"""
        runtime = LambdaRuntime(collection)
        result = runtime.execute(r"(\x.x) y", {}, {"operation": "normalize"}, "")

        assert result.success
        assert result.pretty_print == "y"
        assert result.steps == 1

    def test_runtime_free_vars_operation(self, collection):
        """should execute free-vars operation"""
        runtime = LambdaRuntime(collection)
        result = runtime.execute(r"\x.y", {}, {"operation": "free-vars"}, "")

        assert result.success
        assert result.free_variables == ["y"]

    def test_runtime_is_closed_operation(self, collection):
        """should execute is-closed operation"""
        runtime = LambdaRuntime(collection)
        result = runtime.execute(r"\x.x", {}, {"operation": "is-closed"}, "")

        assert result.success
        assert result.is_closed == True


# ─────────────────────────────────────────────────────────────────────────────
# Cross-Language Parity Tests
# ─────────────────────────────────────────────────────────────────────────────


class TestCrossLanguageParity:
    """Tests that verify identical behavior between Python and JavaScript"""

    def test_beta_reduction_parity(self, collection):
        """Beta reduction should produce same results as JS"""
        test_cases = [
            (r"(\x.x) y", "y"),
            (r"(\x.\y.x) a b", "a"),
            (r"(\x.x x) y", "(y y)"),
        ]

        for expr, expected in test_cases:
            term_hash = parse_lambda_expression(collection, expr)
            result = normalize(collection, term_hash)
            assert result is not None, f"Failed to normalize: {expr}"
            actual = pretty_print_deep(collection, result.normal_form)
            assert (
                actual == expected
            ), f"Expression {expr}: expected {expected}, got {actual}"

    def test_church_numeral_parity(self, collection):
        """Church numerals should be parseable and closed"""
        church_numerals = [
            r"\f.\x.x",  # 0
            r"\f.\x.f x",  # 1
            r"\f.\x.f (f x)",  # 2
        ]

        for i, expr in enumerate(church_numerals):
            term_hash = parse_lambda_expression(collection, expr)
            assert is_closed(collection, term_hash), f"Church {i} should be closed"

    def test_ski_combinators_parity(self, collection):
        """SKI combinators should work correctly"""
        # S = λx.λy.λz.(x z)(y z)
        s = parse_lambda_expression(collection, r"\x.\y.\z.(x z)(y z)")
        assert is_closed(collection, s) == True

        # K = λx.λy.x
        k = parse_lambda_expression(collection, r"\x.\y.x")
        assert is_closed(collection, k) == True

        # I = λx.x
        i = parse_lambda_expression(collection, r"\x.x")
        assert is_closed(collection, i) == True

    def test_free_variables_parity(self, collection):
        """Free variable analysis should match JS"""
        test_cases = [
            ("x", {"x"}),
            (r"\x.x", set()),
            (r"\x.y", {"y"}),
            ("f x", {"f", "x"}),
            (r"\x.x y", {"y"}),
        ]

        for expr, expected in test_cases:
            term_hash = parse_lambda_expression(collection, expr)
            fv = free_variables(collection, term_hash)
            assert fv == expected, f"Expression {expr}: expected {expected}, got {fv}"
