"""Tests for ``tabular-pinned@v1`` (the last reserved-stub π*).

Closes the registry chapter — every modality the substrate paper
reserved (text, claim_lattice, code, arithmetic, logic, time-series,
tabular) is now real. Plus the math substrate extras (algebra-symbolic,
calculus-derivative/integral/limit/series, linear-algebra,
function-sampled).
"""

from __future__ import annotations

import pytest

from arborist.pi_star import PiStarError, get


# ----- basic round-trips -------------------------------------------------


def test_tabular_basic_int_table():
    ps = get("tabular-pinned@v1")
    out = ps.canonicalize(
        b'{"schema":[{"name":"id","type":"int"}],"key_columns":["id"],'
        b'"rows":[[1],[2],[3]]}'
    )
    assert out == b"schema=id:int\nkey=id\nn=3\nrows:\n1\n2\n3"


def test_tabular_empty_rows():
    """Empty body still emits header + n=0."""
    ps = get("tabular-pinned@v1")
    out = ps.canonicalize(
        b'{"schema":[{"name":"id","type":"int"}],"key_columns":["id"],'
        b'"rows":[]}'
    )
    assert out == b"schema=id:int\nkey=id\nn=0\nrows:\n"


def test_tabular_schema_only_no_key_columns():
    """key_columns=[] is valid: rows preserve input order."""
    ps = get("tabular-pinned@v1")
    out = ps.canonicalize(
        b'{"schema":[{"name":"x","type":"str"}],"key_columns":[],'
        b'"rows":[["b"],["a"],["c"]]}'
    )
    assert out == b"schema=x:str\nkey=\nn=3\nrows:\nb\na\nc"


# ----- equivalence-class collapse ----------------------------------------


def test_tabular_row_order_collapses_under_key_sort():
    """Same data, different row order → same canonical (key-sorted)."""
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"id","type":"int"},{"name":"name","type":"str"}],'
        b'"key_columns":["id"],"rows":[[2,"b"],[1,"a"],[3,"c"]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"id","type":"int"},{"name":"name","type":"str"}],'
        b'"key_columns":["id"],"rows":[[1,"a"],[2,"b"],[3,"c"]]}'
    )
    assert a == b


def test_tabular_int_float_string_fold_equivalence():
    """1 ≡ 1.0 ≡ "1.0" for int columns; same canonical."""
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"v","type":"int"}],"key_columns":["v"],'
        b'"rows":[[1]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"v","type":"int"}],"key_columns":["v"],'
        b'"rows":[[1.0]]}'
    )
    c = ps.canonicalize(
        b'{"schema":[{"name":"v","type":"int"}],"key_columns":["v"],'
        b'"rows":[["1.0"]]}'
    )
    assert a == b == c


def test_tabular_rational_fold():
    """0.5 → 1/2; "1/2" → 1/2; same canonical."""
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"r","type":"rational"}],"key_columns":[],'
        b'"rows":[[0.5]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"r","type":"rational"}],"key_columns":[],'
        b'"rows":[["1/2"]]}'
    )
    assert a == b
    assert b"1/2" in a


def test_tabular_bool_normalization():
    """true / True / "true" / 1 (when bool col) — fold to 'true'."""
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"flag","type":"bool"}],"key_columns":[],'
        b'"rows":[[true]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"flag","type":"bool"}],"key_columns":[],'
        b'"rows":[["true"]]}'
    )
    c = ps.canonicalize(
        b'{"schema":[{"name":"flag","type":"bool"}],"key_columns":[],'
        b'"rows":[["yes"]]}'
    )
    assert a == b == c
    assert b"true" in a


# ----- equivalence-class distinction -------------------------------------


def test_tabular_different_schema_distinct():
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"x","type":"int"}],"key_columns":[],'
        b'"rows":[[1]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"y","type":"int"}],"key_columns":[],'
        b'"rows":[[1]]}'
    )
    assert a != b


def test_tabular_different_key_columns_distinct():
    """Same data, different declared key → different canonical (sort changes)."""
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"a","type":"int"},{"name":"b","type":"int"}],'
        b'"key_columns":["a"],"rows":[[2,1],[1,2]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"a","type":"int"},{"name":"b","type":"int"}],'
        b'"key_columns":["b"],"rows":[[2,1],[1,2]]}'
    )
    assert a != b


def test_tabular_header_case_pinned():
    """Header case is part of identity; 'Name' ≠ 'name'."""
    ps = get("tabular-pinned@v1")
    a = ps.canonicalize(
        b'{"schema":[{"name":"Name","type":"str"}],"key_columns":[],'
        b'"rows":[["x"]]}'
    )
    b = ps.canonicalize(
        b'{"schema":[{"name":"name","type":"str"}],"key_columns":[],'
        b'"rows":[["x"]]}'
    )
    assert a != b


# ----- error paths -------------------------------------------------------


def test_tabular_missing_schema_field_raises():
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(b'{"key_columns":[],"rows":[]}')


def test_tabular_unknown_type_raises():
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(
            b'{"schema":[{"name":"x","type":"datetime"}],'
            b'"key_columns":[],"rows":[]}'
        )


def test_tabular_key_column_not_in_schema_raises():
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(
            b'{"schema":[{"name":"a","type":"int"}],'
            b'"key_columns":["b"],"rows":[[1]]}'
        )


def test_tabular_duplicate_column_names_raises():
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(
            b'{"schema":[{"name":"a","type":"int"},{"name":"a","type":"str"}],'
            b'"key_columns":[],"rows":[]}'
        )


def test_tabular_row_arity_mismatch_raises():
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(
            b'{"schema":[{"name":"a","type":"int"},{"name":"b","type":"int"}],'
            b'"key_columns":[],"rows":[[1]]}'
        )


def test_tabular_int_with_non_integer_float_raises():
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(
            b'{"schema":[{"name":"v","type":"int"}],"key_columns":[],'
            b'"rows":[[1.5]]}'
        )


def test_tabular_bool_subtype_of_int_rejected_in_int_column():
    """Python booleans are int-subtypes; we reject so True doesn't
    silently become 1 in an int column."""
    ps = get("tabular-pinned@v1")
    with pytest.raises(PiStarError):
        ps.canonicalize(
            b'{"schema":[{"name":"v","type":"int"}],"key_columns":[],'
            b'"rows":[[true]]}'
        )


# ----- registry presence -------------------------------------------------


def test_tabular_pinned_registered():
    """Sanity: the kernel is in the registry under the expected key."""
    from arborist.pi_star import list_keys
    assert "tabular-pinned@v1" in list_keys()
