"""Tests for clausal.logic.database — step 3."""

import dataclasses
import pytest

from clausal.logic.database import (
    Clause,
    Database,
    Module,
    head_key,
    _flatten_body,
    _extract_param_names,
)
from clausal.terms import And, Call, Compound, Unify as Is, LoadName, Var


# ── Helpers ────────────────────────────────────────────────────────────────────


def call_head(name: str, *args) -> Call:
    """Build a clausal.terms.Call head term for predicate name/arity."""
    return Call(func=LoadName(name=name), args=list(args), kwargs=[])


@dataclasses.dataclass
class foo:
    """Minimal synthetic functor dataclass (simulates one generated by EmbedTransformer)."""
    arg_0: object = None
    arg_1: object = None


@dataclasses.dataclass
class bar:
    x: object = None


# ── head_key ───────────────────────────────────────────────────────────────────


class TestHeadKey:
    def test_compound_ground(self):
        h = Compound("likes", (1, 2))
        assert head_key(h) == ("likes", 2)

    def test_compound_arity_zero(self):
        h = Compound("true", ())
        assert head_key(h) == ("true", 0)

    def test_call_loadname(self):
        h = call_head("member", Var(), Var())
        assert head_key(h) == ("member", 2)

    def test_call_arity_zero(self):
        h = call_head("cut")
        assert head_key(h) == ("cut", 0)

    def test_functor_dataclass(self):
        h = foo(arg_0=1, arg_1=2)
        assert head_key(h) == ("foo", 2)

    def test_functor_dataclass_arity_one(self):
        h = bar(x=42)
        assert head_key(h) == ("bar", 1)

    def test_bad_type_raises(self):
        with pytest.raises(TypeError):
            head_key("not_a_term")

    def test_compound_var_functor_raises(self):
        v = Var()
        with pytest.raises(TypeError):
            head_key(Compound(v, (1,)))


# ── _flatten_body ──────────────────────────────────────────────────────────────


class TestFlattenBody:
    def test_none_body(self):
        assert _flatten_body(None) == []

    def test_single_goal(self):
        goal = Is(left=Var(), right=1)
        assert _flatten_body(goal) == [goal]

    def test_and_chain(self):
        x, y, z = Var(), Var(), Var()
        a = Is(left=x, right=1)
        b = Is(left=y, right=2)
        c = Is(left=z, right=3)
        body = And(left=And(left=a, right=b), right=c)
        assert _flatten_body(body) == [a, b, c]

    def test_right_nested_and(self):
        x, y = Var(), Var()
        a = Is(left=x, right=1)
        b = Is(left=y, right=2)
        body = And(left=a, right=b)
        assert _flatten_body(body) == [a, b]

    def test_true_literal(self):
        assert _flatten_body(True) == [True]

    def test_false_literal(self):
        assert _flatten_body(False) == [False]


# ── Clause ─────────────────────────────────────────────────────────────────────


class TestClause:
    def test_fact_has_empty_body(self):
        head = Compound("true", ())
        c = Clause(head=head, body=[])
        assert c.is_fact()

    def test_rule_is_not_fact(self):
        head = Compound("foo", (1,))
        body = [Is(left=Var(), right=1)]
        c = Clause(head=head, body=body)
        assert not c.is_fact()



# ── Database ───────────────────────────────────────────────────────────────────


class TestDatabase:
    def db(self):
        return Database()

    def fact(self, name, *args):
        return Clause(head=Compound(name, args), body=[])

    def test_assertz_and_clauses_for(self):
        db = self.db()
        c = self.fact("foo", 1)
        db.assertz(c)
        assert db.clauses_for("foo", 1) == [c]

    def test_asserta_prepends(self):
        db = self.db()
        c1, c2 = self.fact("foo", 1), self.fact("foo", 2)
        db.assertz(c1)
        db.asserta(c2)
        assert db.clauses_for("foo", 1) == [c2, c1]

    def test_is_defined_false_before_assert(self):
        db = self.db()
        assert not db.is_defined("foo", 1)

    def test_is_defined_true_after_assert(self):
        db = self.db()
        db.assertz(self.fact("foo", 1))
        assert db.is_defined("foo", 1)

    def test_clauses_for_undefined(self):
        db = self.db()
        assert db.clauses_for("ghost", 0) == []

    def test_get_dispatch_returns_none_when_absent(self):
        db = self.db()
        assert db.get_dispatch("ghost", 0) is None

    def test_retract_removes_clause(self):
        db = self.db()
        c = self.fact("foo", 1)
        db.assertz(c)
        assert db.retract(Compound("foo", (1,))) is True
        assert db.clauses_for("foo", 1) == []

    def test_retract_undefined_predicate(self):
        db = self.db()
        assert db.retract(Compound("ghost", (1,))) is False

    def test_multiple_functors_independent(self):
        db = self.db()
        c1 = self.fact("foo", 1)
        c2 = self.fact("bar", "a", "b")
        db.assertz(c1)
        db.assertz(c2)
        assert db.clauses_for("foo", 1) == [c1]
        assert db.clauses_for("bar", 2) == [c2]

    def test_multiple_clauses_same_predicate(self):
        db = self.db()
        c1 = self.fact("foo", 1)
        c2 = self.fact("foo", 2)
        db.assertz(c1)
        db.assertz(c2)
        assert db.clauses_for("foo", 1) == [c1, c2]

    def test_repr(self):
        db = self.db()
        db.assertz(self.fact("foo", 1))
        db.assertz(self.fact("bar", "a", "b"))
        r = repr(db)
        assert "foo/1" in r
        assert "bar/2" in r


# ── Module ─────────────────────────────────────────────────────────────────────


class TestModule:
    def test_default_database_created(self):
        m = Module("test")
        assert isinstance(m.db, Database)

    def test_assert_fact_compound(self):
        m = Module("test")
        m.assert_fact(Compound("foo", (1, 2)))
        clauses = m.db.clauses_for("foo", 2)
        assert len(clauses) == 1
        assert clauses[0].is_fact()

    def test_assert_fact_call(self):
        m = Module("test")
        term = call_head("edge", 1, 2)
        m.assert_fact(term)
        clauses = m.db.clauses_for("edge", 2)
        assert len(clauses) == 1
        assert clauses[0].head is term

    def test_define_predicate_fact(self):
        """define_predicate with True body → empty body list (fact)."""
        @dataclasses.dataclass
        class MockPredicate:
            head: object
            body: object

        m = Module("test")
        head = Compound("foo", (1,))
        m.define_predicate(MockPredicate(head=head, body=None))
        clauses = m.db.clauses_for("foo", 1)
        assert len(clauses) == 1
        assert clauses[0].is_fact()

    def test_define_predicate_with_body(self):
        @dataclasses.dataclass
        class MockPredicate:
            head: object
            body: object

        m = Module("test")
        head = Compound("append", (1, 2, 3))
        goal1 = Is(left=Var(), right=1)
        goal2 = Is(left=Var(), right=2)
        body = And(left=goal1, right=goal2)
        m.define_predicate(MockPredicate(head=head, body=body))
        clauses = m.db.clauses_for("append", 3)
        assert len(clauses) == 1
        assert clauses[0].body == [goal1, goal2]

    def test_solve_returns_iterator(self):
        """Module.solve delegates to clausal.logic.solve and returns an iterator."""
        m = Module("test")
        result = m.solve(True)
        # True succeeds once; iterator yields one Trail.
        solutions = list(result)
        assert len(solutions) == 1

    def test_repr(self):
        m = Module("mymod")
        assert "mymod" in repr(m)

    def test_functor_dataclass_head_in_define(self):
        """define_predicate works when head is a functor dataclass instance."""
        @dataclasses.dataclass
        class MockPredicate:
            head: object
            body: object

        m = Module("test")
        head = foo(arg_0=Var(), arg_1=Var())
        m.define_predicate(MockPredicate(head=head, body=None))
        clauses = m.db.clauses_for("foo", 2)
        assert len(clauses) == 1


# ── _extract_param_names ────────────────────────────────────────────────────────


class TestExtractParamNames:
    def test_functor_dataclass_returns_field_names(self):
        h = foo(arg_0=1, arg_1=2)
        assert _extract_param_names(h) == ("arg_0", "arg_1")

    def test_single_field_dataclass(self):
        h = bar(x=42)
        assert _extract_param_names(h) == ("x",)

    def test_compound_returns_none(self):
        h = Compound("foo", (1, 2))
        assert _extract_param_names(h) is None

    def test_call_returns_none(self):
        h = call_head("foo", 1, 2)
        assert _extract_param_names(h) is None

    def test_non_term_returns_none(self):
        assert _extract_param_names("not_a_term") is None


# ── Database.register_signature / signature_for ─────────────────────────────────


class TestDatabaseSignature:
    def test_register_and_retrieve(self):
        db = Database()
        db.register_signature("point", 2, ("_x", "_y"))
        assert db.signature_for("point", 2) == ("_x", "_y")

    def test_unknown_predicate_returns_none(self):
        db = Database()
        assert db.signature_for("ghost", 1) is None

    def test_duplicate_identical_is_noop(self):
        db = Database()
        db.register_signature("point", 2, ("_x", "_y"))
        db.register_signature("point", 2, ("_x", "_y"))  # no-op, no warning
        assert db.signature_for("point", 2) == ("_x", "_y")

    def test_conflicting_signature_warns(self):
        db = Database()
        db.register_signature("point", 2, ("_x", "_y"))
        with pytest.warns(UserWarning, match="Signature conflict"):
            db.register_signature("point", 2, ("_lat", "_lon"))
        # Original signature is preserved.
        assert db.signature_for("point", 2) == ("_x", "_y")

    def test_register_creates_signature_if_absent(self):
        db = Database()
        assert not db.is_defined("point", 2)
        db.register_signature("point", 2, ("_x", "_y"))
        # No clauses asserted, but signature is registered.
        assert db.clauses_for("point", 2) == []
        assert db.signature_for("point", 2) == ("_x", "_y")


# ── Module.define_predicate auto-registers signature ────────────────────────────


class TestModuleSignatureRegistration:
    def _pred(self, head, body=None):
        @dataclasses.dataclass
        class MockPredicate:
            head: object
            body: object
        return MockPredicate(head=head, body=body)

    def test_dataclass_head_registers_signature(self):
        m = Module("test")
        m.define_predicate(self._pred(foo(arg_0=Var(), arg_1=Var())))
        assert m.db.signature_for("foo", 2) == ("arg_0", "arg_1")

    def test_compound_head_no_signature(self):
        m = Module("test")
        m.define_predicate(self._pred(Compound("foo", (1, 2))))
        assert m.db.signature_for("foo", 2) is None

    def test_call_head_no_signature(self):
        m = Module("test")
        m.define_predicate(self._pred(call_head("foo", 1, 2)))
        assert m.db.signature_for("foo", 2) is None

    def test_consistent_clauses_same_signature(self):
        m = Module("test")
        m.define_predicate(self._pred(foo(arg_0=Var(), arg_1=Var())))
        m.define_predicate(self._pred(foo(arg_0=Var(), arg_1=Var())))
        assert m.db.signature_for("foo", 2) == ("arg_0", "arg_1")
