"""
Tests for normalize refactoring in testcase_op.py:
- _normalize_scalar_field_by_dist (string/scalar fields)
- _normalize_shape_field_by_dist (shape fields)
- _normalize_range_field_by_dist (range/pair fields)
- Distribution computation (_compute_input/output_distribution)
- flat_* properties after normalize
- ELEWISE/REDUCE output resolution before output_distribution
- Edge cases: None, empty, single float, compressed, already nested, length mismatch
"""

import pytest
from unittest.mock import patch

from ttk.core_modules.testcase_manager.testcase_op import TestcaseOp


def _make_testcase(op_name="Add", input_shapes=((8,), (8,)),
                   input_dtypes=("float16", "float16"),
                   output_shapes=((8,),),
                   output_dtypes=("float16",),
                   const_input_indexes=None,
                   **kwargs):
    case = TestcaseOp()
    case.testcase_name = f"test_{op_name or 'None'}"
    case.op_name = op_name
    case.input_shapes = input_shapes
    case.input_dtypes = input_dtypes
    case.output_shapes = output_shapes
    case.output_dtypes = output_dtypes
    if const_input_indexes is not None:
        case.const_input_indexes = const_input_indexes
    case.input_ori_shapes = kwargs.pop("input_ori_shapes", input_shapes)
    case.output_ori_shapes = kwargs.pop("output_ori_shapes", output_shapes)
    case.attributes = kwargs.pop("attributes", {})
    n_in = len(input_shapes)
    n_out = len(output_shapes or ())
    case.input_formats = kwargs.pop("input_formats", ("ND",) * n_in)
    case.input_ori_formats = kwargs.pop("input_ori_formats", ("ND",) * n_in)
    case.output_formats = kwargs.pop("output_formats", ("ND",) * n_out)
    case.output_ori_formats = kwargs.pop("output_ori_formats", ("ND",) * n_out)
    case.input_data_ranges = kwargs.pop("input_data_ranges", (None,) * n_in)
    for k, v in kwargs.items():
        setattr(case, k, v)
    return case


@pytest.fixture(autouse=True)
def _mock_op_info(monkeypatch):
    monkeypatch.delenv("ASCEND_HOME_PATH", raising=False)
    monkeypatch.delenv("ASCEND_TOOLKIT_HOME", raising=False)
    monkeypatch.delenv("ASCEND_OPP_PATH", raising=False)


def _validate(case):
    n_in = len(case.input_shapes) if case.input_shapes else 0
    n_out = len(case.output_shapes) if case.output_shapes and not isinstance(case.output_shapes, str) else 0
    with patch('ttk.core_modules.operator.op_info_keeper.OpInfoKeeper') as mock:
        mock.return_value.info_of.return_value = {
            "coreType.value": "AiCore",
            "inputs": [{"name": f"i{i}"} for i in range(n_in)],
            "outputs": [{"name": f"o{i}"} for i in range(n_out)],
        }
        case.validate()


# =====================================================================
# Distribution computation
# =====================================================================

class TestDistributionComputation:

    def test_input_dist_nested(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)))
        _validate(case)
        assert case.input_distribution == (2, 0)

    def test_input_dist_flat(self):
        case = _make_testcase(input_shapes=((8,), (8,)))
        _validate(case)
        assert case.input_distribution == (0, 0)

    def test_input_dist_empty(self):
        case = _make_testcase(input_shapes=(), input_dtypes=())
        _validate(case)
        assert case.input_distribution == ()

    def test_output_dist_flat(self):
        case = _make_testcase(output_shapes=((8,),))
        _validate(case)
        assert case.output_distribution == (0,)

    def test_output_dist_nested(self):
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            output_dtypes=(("float16", "float16"),),
        )
        _validate(case)
        assert case.output_distribution == (2,)

    def test_output_dist_string_euclidean(self):
        """ELEWISE resolves before output_distribution is computed."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float16"), "float16"),
            output_shapes="ELEWISE",
            output_dtypes=("float16",),
        )
        _validate(case)
        # ELEWISE resolves to 1 output, flat
        assert isinstance(case.output_shapes, tuple)
        assert case.output_distribution == (0,)

    def test_combined_dist(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float16"), "float16"),
            output_shapes=(((8,), (8,)),),
            output_dtypes=(("float16", "float16"),),
        )
        _validate(case)
        assert case.tensor_list_distribution == (2, 0, 2)


# =====================================================================
# _normalize_scalar_field_by_dist — string/scalar fields
# =====================================================================

class TestNormalizeFieldByDist:

    def test_compressed_broadcast(self):
        """('float16',) broadcasts to match distribution."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16",),
            output_shapes=((8,),),
            output_dtypes=("float16",),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float16"), "float16")

    def test_compressed_single_float(self):
        """Single float absolute_precision broadcasts."""
        case = _make_testcase()
        case.absolute_precision = 1e-5
        _validate(case)
        assert case.absolute_precision == (1e-05,)

    def test_compressed_output_dtypes(self):
        """('float16',) output_dtypes broadcasts with output dist."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            output_dtypes=("float16",),
        )
        _validate(case)
        assert case.output_dtypes == (("float16", "float16"),)

    def test_already_nested_skip(self):
        """Already nested field is not modified."""
        dtypes = (("float16", "float16"), "float16")
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=dtypes,
        )
        _validate(case)
        assert case.input_dtypes == dtypes

    def test_length_mismatch_invalid(self):
        """len(field) > len(dist) → invalid."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16", "float32", "int32"),
        )
        _validate(case)
        assert case.is_valid is False

    def test_empty_field_skip(self):
        """Empty field is skipped, no normalize applied."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16", "float16"),
            input_formats=(),
        )
        _validate(case)
        # input_formats is empty — normalize skips it
        assert case.input_formats == ()


# =====================================================================
# _normalize_shape_field_by_dist — shape fields
# =====================================================================

class TestNormalizeShapeField:

    def test_compressed_shape_broadcast(self):
        """Single shape broadcasts to all positions."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_ori_shapes=((3, 4),),
        )
        _validate(case)
        # (3,4) looks like a single shape, len=1, dist=(2,0), flat_count=3
        # broadcast to (3,4),(3,4),(3,4) then re-nest
        assert case.input_ori_shapes == (((3, 4), (3, 4)), (3, 4))

    def test_already_nested_shape_skip(self):
        """Already nested shape field is not modified."""
        ori = (((3, 4), (5, 4)), (8,))
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_ori_shapes=ori,
        )
        _validate(case)
        assert case.input_ori_shapes == ori

    def test_none_element_in_shape(self):
        """Mixed None and shape elements."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_ori_shapes=(None, None),
        )
        _validate(case)
        # len=2, dist=(2,0), flat_count=3.
        # _is_shape_field_already_nested: dist[0]=2, val=None → not tuple → False
        # len=2 < flat_count=3, len==len(dist)=2 → flatten_by_distribution
        # flatten_by_distribution((None, None), (2,0)) → (None, None)
        # re-nest: ((None, None), None) — but None is not a valid shape for TensorList
        # This is an edge case; just verify no crash
        assert case.is_valid


# =====================================================================
# _normalize_range_field_by_dist — range/pair fields
# =====================================================================

class TestNormalizeRangeField:

    def test_compressed_range_broadcast(self):
        """Single range broadcasts to all tensors in TensorList."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_data_ranges=((0, 1),),
        )
        _validate(case)
        assert case.input_data_ranges == (((0, 1), (0, 1)), (0, 1))

    def test_already_nested_range_skip(self):
        """Already nested ranges are not modified."""
        ranges = (((None, 1.0), (-1.0, 1.0)), (0.0, 5.0))
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float16"), "float16"),
            input_data_ranges=ranges,
        )
        _validate(case)
        assert case.input_data_ranges == ranges

    def test_precision_tolerances_compressed(self):
        """Compressed precision_tolerances broadcast."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            precision_tolerances=((0.004, 0.004),),
        )
        _validate(case)
        assert case.precision_tolerances == (((0.004, 0.004), (0.004, 0.004)),)

    def test_precision_tolerances_pair_not_destructed(self):
        """(rtol, ptol) pairs must not be destructed during normalize."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            precision_tolerances=(((0.001, 0.002), (0.003, 0.004)),),
        )
        _validate(case)
        # Already nested — preserved
        assert case.precision_tolerances == (((0.001, 0.002), (0.003, 0.004)),)

    def test_non_tuple_field_skip(self):
        """Non-tuple field (e.g. float absolute_precision) is wrapped."""
        case = _make_testcase()
        case.precision_tolerances = None
        _validate(case)
        assert case.precision_tolerances is None

    def test_bare_scalar_broadcast(self):
        """1e-5 bare scalar → broadcast to all positions."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
        )
        case.absolute_precision = 1e-5
        _validate(case)
        assert case.absolute_precision == ((1e-5, 1e-5),)


# =====================================================================
# flat_* properties after normalize
# =====================================================================

class TestFlatPropertiesAfterNormalize:

    def test_flat_input_shapes_nested(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)))
        _validate(case)
        assert case.flat_input_shapes == ((3, 4), (5, 4), (8,))

    def test_flat_input_dtypes_compressed(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16",),
        )
        _validate(case)
        assert case.flat_input_dtypes == ("float16", "float16", "float16")

    def test_flat_input_dtypes_already_nested(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float32"), "int32"),
        )
        _validate(case)
        assert case.flat_input_dtypes == ("float16", "float32", "int32")

    def test_flat_output_dtypes_compressed(self):
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            output_dtypes=("float16",),
        )
        _validate(case)
        assert case.flat_output_dtypes == ("float16", "float16")

    def test_flat_input_data_ranges_compressed(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_data_ranges=((0, 1),),
        )
        _validate(case)
        assert case.flat_input_data_ranges == ((0, 1), (0, 1), (0, 1))

    def test_flat_input_data_ranges_nested(self):
        ranges = (((None, 1.0), (-1.0, 1.0)), (0.0, 5.0))
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float16"), "float16"),
            input_data_ranges=ranges,
        )
        _validate(case)
        assert case.flat_input_data_ranges == ((None, 1.0), (-1.0, 1.0), (0.0, 5.0))

    def test_flat_precision_tolerances_nested(self):
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            precision_tolerances=(((0.001, 0.002), (0.003, 0.004)),),
        )
        _validate(case)
        assert case.flat_precision_tolerances == ((0.001, 0.002), (0.003, 0.004))

    def test_flat_absolute_precision_single_float(self):
        case = _make_testcase()
        case.absolute_precision = 1e-5
        _validate(case)
        assert case.flat_absolute_precision == (1e-05,)

    def test_flat_absolute_precision_tuple(self):
        case = _make_testcase(output_shapes=((8,), (9,)))
        case.absolute_precision = (1e-5, 1e-6)
        _validate(case)
        assert case.flat_absolute_precision == (1e-5, 1e-6)

    def test_flat_absolute_precision_per_output_no_double_wrap(self):
        """Per-output tuple (e.g. from result CSV) must not be double-nested."""
        case = _make_testcase(output_shapes=((8,), (9,), (10,)))
        case.absolute_precision = (1e-5, 1e-5, 1e-5)
        _validate(case)
        assert case.flat_absolute_precision == (1e-5, 1e-5, 1e-5)
        # Every element must be a scalar float, not a nested tuple
        for val in case.flat_absolute_precision:
            assert isinstance(val, float), f"Expected float, got {type(val)}: {val}"

    def test_flat_input_ori_shapes_nested(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_ori_shapes=(((3, 4), (5, 4)), (8,)),
        )
        _validate(case)
        assert case.flat_input_ori_shapes == ((3, 4), (5, 4), (8,))

    def test_flat_output_formats_compressed(self):
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            output_formats=("ND",),
        )
        _validate(case)
        assert case.flat_output_formats == ("ND", "ND")

    def test_no_dist_flat_identity(self):
        """Flat (non-nested) with no dist — properties return as-is."""
        case = _make_testcase(input_shapes=((8,), (8,)))
        _validate(case)
        assert case.flat_input_shapes == ((8,), (8,))
        assert case.flat_input_dtypes == ("float16", "float16")


# =====================================================================
# ELEWISE/REDUCE resolution order
# =====================================================================

class TestInferenceResolutionOrder:

    def test_elewise_resolves_before_output_dist(self):
        """output_shapes='ELEWISE' resolved before output_distribution computed."""
        case = _make_testcase(
            input_shapes=((8,), (8,)),
            input_dtypes=("float16", "float16"),
            output_shapes="ELEWISE",
            output_dtypes=("float16",),
        )
        _validate(case)
        assert isinstance(case.output_shapes, tuple)
        assert len(case.output_shapes) == 1
        assert case.output_distribution == (0,)

    def test_elewise_output_dtypes_normalized(self):
        """After ELEWISE resolves, output_dtypes gets normalized."""
        case = _make_testcase(
            input_shapes=((8,), (8,)),
            input_dtypes=("float16", "float16"),
            output_shapes="ELEWISE",
            output_dtypes=("float16",),
        )
        _validate(case)
        assert case.output_dtypes == ("float16",)

    def test_reduce_resolves(self):
        """REDUCE resolves correctly."""
        case = _make_testcase(
            input_shapes=((8, 4),),
            input_dtypes=("float16",),
            output_shapes="REDUCE",
            output_dtypes=("float16",),
            attributes={"axis": (1,)},
        )
        _validate(case)
        assert isinstance(case.output_shapes, tuple)
        assert case.output_distribution == (0,)


# =====================================================================
# Edge cases
# =====================================================================

class TestNormalizeEdgeCases:

    def test_empty_input_shapes(self):
        case = _make_testcase(input_shapes=(), input_dtypes=(), input_ori_shapes=())
        _validate(case)
        assert case.input_distribution == ()

    def test_output_shapes_none(self):
        case = _make_testcase()
        case.output_shapes = None
        _validate(case)
        assert case.is_valid is False

    def test_multiple_tensorlists(self):
        """Multiple TensorList positions in input."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), ((8,), (9,), (10,))),
            input_dtypes=("float16",),
        )
        _validate(case)
        assert case.input_distribution == (2, 3)
        assert case.input_dtypes == (("float16", "float16"), ("float16", "float16", "float16"))
        assert case.flat_input_dtypes == ("float16", "float16", "float16", "float16", "float16")

    def test_short_field_no_pad_invalid(self):
        """input_dtypes has no pad value — fewer values than dist → invalid."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,), (9,)),
            input_dtypes=("float16", "float32"),
        )
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"

    def test_precision_tolerances_flat_no_output_dist(self):
        """precision_tolerances with no output_distribution stays as-is."""
        case = _make_testcase(output_shapes=((8,),))
        case.precision_tolerances = ((0.004, 0.004),)
        _validate(case)
        # output_dist = (0,), len matches → _is_range_field_already_nested checks...
        # dist[0]=0, so no TensorList check → returns True → skip
        assert case.precision_tolerances == ((0.004, 0.004),)

    def test_all_none_fields(self):
        """Optional fields are None/empty — no crash during normalize."""
        case = _make_testcase()
        case.input_ori_shapes = None
        case.input_data_ranges = None
        case.precision_tolerances = None
        case.absolute_precision = None
        _validate(case)
        assert case.is_valid


# =====================================================================
# Per-TensorList compressed broadcast
# =====================================================================

class TestPerTensorListCompressed:
    """TensorList position has compressed (len-1) value that needs broadcast."""

    def test_input_dtype_per_tl_compressed(self):
        """('float32',) at TensorList(2) position → ('float32','float32')."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16",), "float32"),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float16"), "float32")
        assert case.flat_input_dtypes == ("float16", "float16", "float32")

    def test_input_format_per_tl_compressed(self):
        """('ND',) at TensorList(2) position → ('ND','ND')."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_formats=(("ND",), "NZ"),
        )
        _validate(case)
        assert case.input_formats == (("ND", "ND"), "NZ")

    def test_output_dtype_per_tl_compressed(self):
        """('float16',) at output TensorList(2) position → ('float16','float16')."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            output_dtypes=(("float16",),),
        )
        _validate(case)
        assert case.output_dtypes == (("float16", "float16"),)
        assert case.flat_output_dtypes == ("float16", "float16")

    def test_mixed_compressed_and_expanded(self):
        """One position compressed, another fully expanded."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float32"), "int32"),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float32"), "int32")

    def test_multiple_tensorlists_both_compressed(self):
        """Two TensorList positions, both with compressed dtype."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), ((8,), (9,), (10,))),
            input_dtypes=(("float16",), ("int32",)),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float16"), ("int32", "int32", "int32"))
        assert case.flat_input_dtypes == ("float16", "float16", "int32", "int32", "int32")

    def test_scalar_at_tensorlist_position(self):
        """Scalar value at TensorList position broadcast to match count."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16", "float32"),
        )
        _validate(case)
        # len=2, dist=(2,0), len(field)==len(dist) → flatten_by_distribution
        # "float16" at dist=2 → broadcast to ["float16","float16"]
        # "float32" at dist=0 → ["float32"]
        # flat = ("float16","float16","float32") → re-nest → (("float16","float16"),"float32")
        assert case.input_dtypes == (("float16", "float16"), "float32")


# =====================================================================
# Non-TensorList multi-output compressed broadcast
# =====================================================================

class TestNonTensorListMultiOutputCompressed:
    """Flat (non-nested) outputs with compressed single dtype/value."""

    def test_dtype_broadcast_5_outputs(self):
        """('uint16',) with 5 flat outputs → 5-element tuple."""
        case = _make_testcase(
            output_shapes=((8,), (8,), (8,), (8,), (8,)),
            output_dtypes=("uint16",),
        )
        _validate(case)
        assert case.output_dtypes == ("uint16", "uint16", "uint16", "uint16", "uint16")
        assert case.flat_output_dtypes == ("uint16", "uint16", "uint16", "uint16", "uint16")

    def test_format_broadcast_3_outputs(self):
        """('ND',) with 3 flat outputs → 3-element tuple."""
        case = _make_testcase(
            output_shapes=((8,), (16,), (32,)),
            output_formats=("ND",),
        )
        _validate(case)
        assert case.output_formats == ("ND", "ND", "ND")

    def test_single_output_no_broadcast(self):
        """Single output with single dtype — no broadcast needed."""
        case = _make_testcase(
            output_shapes=((8, 1716),),
            output_dtypes=("int32",),
        )
        _validate(case)
        assert case.output_dtypes == ("int32",)

    def test_per_param_compressed_3_outputs_invalid(self):
        """output_dtypes has no pad value — 2 dtypes for 3 outputs → invalid."""
        case = _make_testcase(
            output_shapes=((8,), (16,), (32,)),
            output_dtypes=("float16", "float32"),
        )
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"

    def test_input_dtype_broadcast_flat(self):
        """('float32',) with 3 flat inputs → 3-element tuple."""
        case = _make_testcase(
            input_shapes=((8,), (16,), (32,)),
            input_dtypes=("float32",),
        )
        _validate(case)
        assert case.input_dtypes == ("float32", "float32", "float32")


# =====================================================================
# _is_field_already_nested — prevent double-normalize
# =====================================================================

class TestIsFieldAlreadyNested:
    """Tests for already-nested detection via _normalize_field_by_dist."""

    def _assert_already_nested_scalar(self, field, dist):
        """Verify scalar field is detected as already-nested (no modification)."""
        case = TestcaseOp()
        case.is_valid = True
        case.input_dtypes = field
        case._normalize_field_by_dist("input_dtypes", dist, TestcaseOp._is_scalar_group)
        assert case.is_valid is True
        assert case.input_dtypes == field

    def _assert_not_already_nested_scalar(self, field, dist):
        """Verify scalar field is NOT already-nested (gets normalized or rejected)."""
        case = TestcaseOp()
        case.is_valid = True
        case.input_dtypes = field
        case._normalize_field_by_dist("input_dtypes", dist, TestcaseOp._is_scalar_group)
        assert case.input_dtypes != field or case.is_valid is False

    def _assert_already_nested_range(self, field, dist):
        """Verify range field is detected as already-nested (no modification)."""
        case = TestcaseOp()
        case.is_valid = True
        case.input_data_ranges = field
        case._normalize_field_by_dist("input_data_ranges", dist, TestcaseOp._is_range_group)
        assert case.is_valid is True
        assert case.input_data_ranges == field

    def _assert_not_already_nested_range(self, field, dist):
        """Verify range field is NOT already-nested (gets normalized or rejected)."""
        case = TestcaseOp()
        case.is_valid = True
        case.input_data_ranges = field
        case._normalize_field_by_dist("input_data_ranges", dist, TestcaseOp._is_range_group)
        assert case.input_data_ranges != field or case.is_valid is False

    def test_fully_nested_matches_dist(self):
        self._assert_already_nested_scalar(
            (("float16", "float16"), "float16"), (2, 0))

    def test_compressed_not_nested(self):
        self._assert_not_already_nested_scalar(("float16",), (2, 0))

    def test_len_mismatch(self):
        self._assert_not_already_nested_scalar(("float16", "float16"), (3,))

    def test_inner_len_mismatch(self):
        """Per-TensorList compressed: inner len != dist count → not already nested."""
        self._assert_not_already_nested_scalar(
            (("float16",), "float16"), (2, 0))

    def test_all_single_tensors(self):
        """Flat (no TensorList) — all-zero dist, matching length → already nested."""
        self._assert_already_nested_scalar(("float16", "float16"), (0, 0))

    def test_single_tensorlist(self):
        self._assert_already_nested_scalar((("a", "b", "c"),), (3,))

    def test_range_fully_nested(self):
        """Range field already nested."""
        self._assert_already_nested_range(
            (((0.0, 1.0), (0.5, 1.0)), (0.0, 5.0)), (2, 0))

    def test_range_non_group_tuple_not_nested(self):
        """(0.0, 1.0) at dist=2 position is not a range group → not already nested."""
        self._assert_not_already_nested_range(
            ((0.0, 1.0), (0.0, 5.0)), (2, 0))


# =====================================================================
# _flatten_by_distribution — per-TensorList broadcast
# =====================================================================

class TestFlattenByDistribution:
    """Tests for _flatten_by_distribution static method."""

    def test_flat_values_flat_dist(self):
        result = TestcaseOp._flatten_by_distribution(("a", "b"), (0, 0))
        assert result == ("a", "b")

    def test_expand_tuple_list(self):
        result = TestcaseOp._flatten_by_distribution(("a", ("b", "c")), (0, 2))
        assert result == ("a", "b", "c")

    def test_broadcast_single_in_list(self):
        """('b',) at dist=2 position → broadcast to ('b','b')."""
        result = TestcaseOp._flatten_by_distribution(("a", ("b",)), (0, 2))
        assert result == ("a", "b", "b")

    def test_scalar_to_tensorlist(self):
        """Scalar at dist=2 position → broadcast to 2 copies."""
        result = TestcaseOp._flatten_by_distribution(("a", "b"), (2, 0))
        assert result == ("a", "a", "b")

    def test_mixed(self):
        result = TestcaseOp._flatten_by_distribution(
            (("x", "y"), "z"), (2, 0))
        assert result == ("x", "y", "z")


# =====================================================================
# Normalize is idempotent (no double-normalize)
# =====================================================================

class TestNormalizeIdempotent:
    """Verify that normalize on already-normalized fields is a no-op."""

    def test_fully_nested_dtypes_not_double_expanded(self):
        dtypes = (("float16", "float16"), "float16")
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=dtypes,
        )
        _validate(case)
        assert case.input_dtypes == dtypes

    def test_normalized_then_normalize_again(self):
        """Calling _normalize_compressed_fields twice produces same result."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16",),
        )
        _validate(case)
        first = case.input_dtypes
        # Normalize again
        case._normalize_compressed_fields()
        assert case.input_dtypes == first


# =====================================================================
# append_output_dtype — interface test
# =====================================================================

class TestAppendOutputMetadata:
    """Tests for append_output_metadata: encapsulates metadata append with cache consistency."""

    def test_flat_output_no_dist(self):
        """1 output, no TensorList → all flat properties include appended metadata."""
        case = _make_testcase(
            output_shapes=((8, 1716),),
            output_dtypes=("int32",),
        )
        _validate(case)
        assert case.flat_output_dtypes == ("int32",)
        case.append_output_metadata("uint64", "ND", (9,))
        assert case.output_dtypes == ("int32", "uint64")
        assert case.flat_output_dtypes == ("int32", "uint64")
        assert case.flat_output_formats[-1] == "ND"
        assert case.flat_output_shapes[-1] == (9,)

    def test_flat_5_outputs_no_dist(self):
        """5 outputs, no TensorList → flat properties include appended metadata."""
        case = _make_testcase(
            output_shapes=((8,), (8,), (8,), (8,), (8,)),
            output_dtypes=("uint16",),
        )
        _validate(case)
        case.append_output_metadata("uint64", "ND", (9,))
        assert case.flat_output_dtypes == ("uint16",) * 5 + ("uint64",)

    def test_tensorlist_output(self):
        """TensorList output → flat properties flatten then append metadata."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            output_dtypes=(("float16",),),
        )
        _validate(case)
        assert case.flat_output_dtypes == ("float16", "float16")
        case.append_output_metadata("uint64", "ND", (9,))
        assert case.output_dtypes == (("float16", "float16"), "uint64")
        assert case.flat_output_dtypes == ("float16", "float16", "uint64")
        assert case.flat_output_shapes == ((8,), (8,), (9,))

    def test_mixed_tensorlist_and_flat(self):
        """TensorList + flat output → flat correct after append."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            output_shapes=(((8,), (8,)), (4,)),
            output_dtypes=("float16",),
        )
        _validate(case)
        case.append_output_metadata("uint64", "ND", (9,))
        assert case.flat_output_dtypes == ("float16", "float16", "float16", "uint64")

    def test_cache_hit_refreshed(self):
        """flat properties accessed before append, then after — cache refreshed."""
        case = _make_testcase(
            output_shapes=((8,),),
            output_dtypes=("int32",),
        )
        _validate(case)
        _ = case.flat_output_dtypes  # populate cache
        case.append_output_metadata("uint64", "ND", (9,))
        assert case.flat_output_dtypes == ("int32", "uint64")


# =====================================================================
# _normalize_manual_binaries
# =====================================================================

class TestNormalizeManualBinaries:

    def test_empty_not_modified(self):
        case = _make_testcase()
        case.manual_input_binaries = ()
        _validate(case)
        assert case.manual_input_binaries == ()

    def test_none_not_modified(self):
        case = _make_testcase()
        case.manual_input_binaries = None
        _validate(case)
        assert case.manual_input_binaries is None

    def test_single_string_wrapped(self):
        case = _make_testcase(input_shapes=((8,),),
                              input_dtypes=("float16",))
        case.manual_input_binaries = 'ax.csv'
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv',)

    def test_flat_tuple_preserved(self):
        case = _make_testcase(input_shapes=((8,), (8,), (8,)),
                              input_dtypes=("float16", "float16", "float16"))
        case.manual_input_binaries = ('ax.csv', 'ay.csv', 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', 'ay.csv', 'az.csv')

    def test_none_unquoted_preserved(self):
        case = _make_testcase(input_shapes=((8,), None, (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('ax.csv', None, 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', None, 'az.csv')

    def test_none_quoted_converted(self):
        case = _make_testcase(input_shapes=((8,), None, (8,)),
                              input_dtypes=("float16", "float16", "float32"))
        case.manual_input_binaries = ('ax.csv', 'None', 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', None, 'az.csv')

    def test_nested_tuple_preserved(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', 'ay.csv'), 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', 'ay.csv'), 'az.csv')

    def test_nested_with_none_quoted(self):
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', 'None'), 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', None), 'az.csv')

    def test_nested_with_none_unquoted(self):
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', None), 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', None), 'az.csv')

    def test_list_converted_to_tuple(self):
        case = _make_testcase()
        case.manual_input_binaries = ['ax.csv', 'ay.csv']
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', 'ay.csv')
        assert isinstance(case.manual_input_binaries, tuple)

    def test_nested_list_converted_to_tuple(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (['ax.csv', 'ay.csv'], 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', 'ay.csv'), 'az.csv')
        assert isinstance(case.manual_input_binaries[0], tuple)

    # --- validation error cases ---

    def test_nested_rejected_without_tensor_list(self):
        case = _make_testcase(input_shapes=((8,), (8,)),
                              input_dtypes=("float16", "float16"))
        case.manual_input_binaries = (('ax.csv', 'ay.csv'), 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_mixed_nested_flat_rejected(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('ax.csv', ('az.csv',))
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_flat_count_exceeds_inputs_rejected(self):
        case = _make_testcase(input_shapes=((8,), (8,)),
                              input_dtypes=("float16", "float16"))
        case.manual_input_binaries = ('ax.csv', 'ay.csv', 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_flat_with_none_input_explicit_none_ok(self):
        case = _make_testcase(input_shapes=((8,), None, (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('ax.csv', None, 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', None, 'az.csv')

    def test_flat_missing_required_input_rejected(self):
        case = _make_testcase(input_shapes=((8,), (8,), (8,)),
                              input_dtypes=("float16", "float16", "float16"))
        case.manual_input_binaries = ('ax.csv', 'ay.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_flat_file_for_none_input_rejected(self):
        case = _make_testcase(input_shapes=((8,), None, (8,)),
                              input_dtypes=("float16", "float16", "float32"))
        case.manual_input_binaries = ('ax.csv', 'unexpected.csv', 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_nested_group_count_exceeds_inputs_rejected(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', 'ay.csv', 'extra.csv'), 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_nested_with_optional_input_none_ok(self):
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', None), 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', None), 'az.csv')

    def test_nested_missing_required_input_rejected(self):
        """TensorList group has 2 non-None inputs but only 1 file."""
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv',), 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    # --- reshape verification: binaries always match input_shapes structure ---

    def test_flat_input_reshaped_to_nested(self):
        """Flat binaries for TensorList inputs → reshaped to match input_shapes."""
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('ax.csv', 'ay.csv', 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', 'ay.csv'), 'az.csv')

    def test_flat_with_none_padded_to_nested(self):
        """Flat binaries with None → reshaped and padded to match input_shapes."""
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('ax.csv', None, 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', None), 'az.csv')

    def test_no_tensor_list_stays_flat(self):
        """No TensorList → binaries stays flat tuple."""
        case = _make_testcase(input_shapes=((8,), (8,)),
                              input_dtypes=("float16", "float16"))
        case.manual_input_binaries = ('ax.csv', 'ay.csv')
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', 'ay.csv')

    def test_single_input_stays_single(self):
        case = _make_testcase(input_shapes=((8,),),
                              input_dtypes=("float16",))
        case.manual_input_binaries = 'ax.csv'
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv',)

    # --- more None scenarios ---

    def test_flat_all_none_inputs_all_none_binaries(self):
        case = _make_testcase(input_shapes=(None, None),
                              input_dtypes=("float16", "float16"))
        case.manual_input_binaries = (None, None)
        _validate(case)
        assert case.manual_input_binaries == (None, None)

    def test_flat_trailing_none_input(self):
        case = _make_testcase(input_shapes=((8,), (8,), None),
                              input_dtypes=("float16", "float16", "float16"))
        case.manual_input_binaries = ('ax.csv', 'ay.csv', None)
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', 'ay.csv', None)

    def test_tensor_list_group_none_mixed_nested(self):
        """TensorList with one None and one non-None input."""
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', None), 'az.csv')
        _validate(case)
        assert case.manual_input_binaries == (('ax.csv', None), 'az.csv')

    def test_flat_binaries_exceed_non_tensor_list_inputs_rejected(self):
        """3 flat binaries for 2 non-TensorList inputs (None, None not recognized as TensorList)."""
        case = _make_testcase(input_shapes=((None, None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (None, None, 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_trailing_none_inputs_implicit_none_binaries(self):
        """Binaries shorter because trailing inputs are all None → padded."""
        case = _make_testcase(input_shapes=((8,), None, None),
                              input_dtypes=("float16", "float16", "float16"))
        case.manual_input_binaries = ('ax.csv',)
        _validate(case)
        assert case.manual_input_binaries == ('ax.csv', None, None)

    # --- more invalid scenarios ---

    def test_nested_top_level_count_mismatch_rejected(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', 'ay.csv'),)
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_nested_tensor_list_position_is_str_rejected(self):
        """Nested format but TensorList position is str instead of tuple."""
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('ax.csv', 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_nested_non_tensor_list_position_is_tuple_rejected(self):
        """Non-TensorList position given as tuple."""
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', 'ay.csv'), ('az.csv',))
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_invalid_type_rejected(self):
        case = _make_testcase(input_shapes=((8,),),
                              input_dtypes=("float16",))
        case.manual_input_binaries = 123
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_file_for_none_in_tensor_list_group_rejected(self):
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', 'unexpected.csv'), 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"

    def test_missing_file_for_non_none_in_tensor_list_group_rejected(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('ax.csv', None), 'az.csv')
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "MANUAL_BINARIES_INVALID"


# =====================================================================
# flat_manual_input_binaries property
# =====================================================================

class TestFlatManualInputBinaries:
    """Tests for flat_manual_input_binaries — uses deep_flatten."""

    def test_flat_no_dist(self):
        case = _make_testcase(input_shapes=((8,), (8,)),
                              input_dtypes=("float16", "float16"))
        case.manual_input_binaries = ('a.csv', 'b.csv')
        _validate(case)
        assert case.flat_manual_input_binaries == ('a.csv', 'b.csv')

    def test_nested_tensorlist(self):
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('a.csv', 'b.csv'), 'c.csv')
        _validate(case)
        assert case.flat_manual_input_binaries == ('a.csv', 'b.csv', 'c.csv')

    def test_nested_with_none(self):
        case = _make_testcase(input_shapes=(((3, 4), None), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = (('a.csv', None), 'c.csv')
        _validate(case)
        assert case.flat_manual_input_binaries == ('a.csv', None, 'c.csv')

    def test_none_when_not_set(self):
        case = _make_testcase()
        _validate(case)
        assert case.flat_manual_input_binaries is None

    def test_empty_tuple(self):
        case = _make_testcase()
        case.manual_input_binaries = ()
        _validate(case)
        assert case.flat_manual_input_binaries == ()

    def test_flat_reshaped_to_nested_then_flat(self):
        """Flat binaries → reshape to nested → flat property returns flat."""
        case = _make_testcase(input_shapes=(((3, 4), (5, 4)), (8,)),
                              input_dtypes=(("float16", "float16"), "float32"))
        case.manual_input_binaries = ('a.csv', 'b.csv', 'c.csv')
        _validate(case)
        assert case.manual_input_binaries == (('a.csv', 'b.csv'), 'c.csv')
        assert case.flat_manual_input_binaries == ('a.csv', 'b.csv', 'c.csv')


# =====================================================================
# Unified _normalize_field_by_dist — TDD tests
# =====================================================================

class TestIsScalarGroup:
    """Tests for _is_scalar_group classmethod."""

    def test_tuple_is_group(self):
        assert TestcaseOp._is_scalar_group(('NCHW', 'NHWC')) is True

    def test_list_is_group(self):
        assert TestcaseOp._is_scalar_group(['NCHW', 'NHWC']) is True

    def test_string_is_not_group(self):
        assert TestcaseOp._is_scalar_group('NCHW') is False

    def test_int_is_not_group(self):
        assert TestcaseOp._is_scalar_group(42) is False

    def test_float_is_not_group(self):
        assert TestcaseOp._is_scalar_group(1e-5) is False

    def test_empty_tuple_is_not_group(self):
        assert TestcaseOp._is_scalar_group(()) is True


class TestIsRangeGroup:
    """Tests for _is_range_group classmethod."""

    def test_range_list_is_group(self):
        assert TestcaseOp._is_range_group(((0.0, 1.0), (0.5, 1.0))) is True

    def test_single_range_is_not_group(self):
        """A bare range (0.0, 1.0) is NOT a group — it's a single atomic value."""
        assert TestcaseOp._is_range_group((0.0, 1.0)) is False

    def test_string_tuple_is_not_group(self):
        assert TestcaseOp._is_range_group(('NCHW', 'NHWC')) is False

    def test_empty_tuple_is_not_group(self):
        assert TestcaseOp._is_range_group(()) is False

    def test_single_element_range_list(self):
        """((0.0, 1.0),) is a 1-element range group."""
        assert TestcaseOp._is_range_group(((0.0, 1.0),)) is True

    def test_int_tuple_is_not_group(self):
        """(1, 2) looks like a range but val[0]=1 is not tuple → not a group."""
        assert TestcaseOp._is_range_group((1, 2)) is False


class TestNormalizeFieldAlreadyNested:
    """Already-nested detection via unified _normalize_field_by_dist."""

    def test_scalar_fully_nested_skip(self):
        """Already nested scalar field — no modification."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float32"), "int32"),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float32"), "int32")

    def test_range_fully_nested_skip(self):
        """Already nested range field — no modification."""
        ranges = (((None, 1.0), (-1.0, 1.0)), (0.0, 5.0))
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float16"), "float16"),
            input_data_ranges=ranges,
        )
        _validate(case)
        assert case.input_data_ranges == ranges

    def test_all_flat_dist_zero_already_nested(self):
        """Flat (no TensorList) with matching length — already nested."""
        case = _make_testcase(
            input_shapes=((8,), (8,)),
            input_dtypes=("float16", "float32"),
        )
        _validate(case)
        assert case.input_dtypes == ("float16", "float32")


class TestNormalizeFieldLenEqOne:
    """len(field)==1 branch: unwrap, reject, broadcast."""

    def test_scalar_compressed_broadcast(self):
        """('float16',) broadcasts to match distribution."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16",),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float16"), "float16")

    def test_scalar_single_element_group_unwrap(self):
        """(('NCHW',),) → unwrap to 'NCHW' → broadcast."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_formats=(("ND",),),
        )
        _validate(case)
        assert case.input_formats == (("ND", "ND"), "ND")

    def test_scalar_multi_element_group_rejected(self):
        """Bug fix: (('NCHW','NHWC'),) with dist=(2,0) → REJECT.

        Old code would broadcast ('NCHW','NHWC') to position 1 (num=0),
        producing a tuple where a single string is expected. Now correctly
        rejected as CASE_FIELD_AMBIGUOUS.
        """
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
        )
        case.input_dtypes = (("float16", "float32"),)
        # Directly call unified method
        case._normalize_field_by_dist(
            "input_dtypes", (2, 0), TestcaseOp._is_scalar_group)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"

    def test_range_compressed_broadcast(self):
        """Single range ((0,1),) broadcasts to all positions."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_data_ranges=((0, 1),),
        )
        _validate(case)
        assert case.input_data_ranges == (((0, 1), (0, 1)), (0, 1))

    def test_range_single_element_group_unwrap(self):
        """(((0.0,1.0),),) → unwrap to (0.0,1.0) → broadcast."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
        )
        case.input_data_ranges = (((0.0, 1.0),),)
        # Directly call to test unwrap path
        case._normalize_field_by_dist(
            "input_data_ranges", (2, 0), TestcaseOp._is_range_group)
        assert case.input_data_ranges == (((0.0, 1.0), (0.0, 1.0)), (0.0, 1.0))

    def test_range_multi_element_group_rejected(self):
        """(((0.0,1.0),(0.5,1.0)),) with dist=(2,0) → REJECT."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
        )
        case.input_data_ranges = (((0.0, 1.0), (0.5, 1.0)),)
        case._normalize_field_by_dist(
            "input_data_ranges", (2, 0), TestcaseOp._is_range_group)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"


class TestNormalizeFieldLenBetweenOneAndDist:
    """1 < len(field) < len(dist): pad or reject."""

    def test_scalar_short_field_no_pad_rejected(self):
        """input_dtypes has no pad — fewer values than dist → invalid."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,), (9,)),
            input_dtypes=("float16", "float32"),
        )
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"

    def test_range_short_field_with_pad(self):
        """1 < len(field) < len(dist): pad missing positions with pad_value."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)), (4,), (6,)),
        )
        case.precision_tolerances = ((0.001, 0.002), (0.003, 0.004))
        # len(field)=2, len(dist)=3, pad with None
        case._normalize_field_by_dist(
            "precision_tolerances", (2, 0, 0), TestcaseOp._is_range_group,
            pad_value=None)
        assert case.precision_tolerances == (
            ((0.001, 0.002), (0.001, 0.002)), (0.003, 0.004), None)


class TestNormalizeFieldLenGtDist:
    """len(field) > len(dist) → REJECT."""

    def test_scalar_len_gt_dist(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16", "float32", "int32"),
        )
        _validate(case)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"

    def test_range_len_gt_dist(self):
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
        )
        case.input_data_ranges = ((0, 1), (-1, 1), (0, 5))
        case._normalize_field_by_dist(
            "input_data_ranges", (2, 0), TestcaseOp._is_range_group)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"


class TestNormalizeFieldPerPositionExpand:
    """Per-position expansion after broadcast/pad."""

    def test_scalar_group_matches_dist(self):
        """Group at TensorList position with matching len → use as-is."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16", "float32"), "int32"),
        )
        _validate(case)
        # Already nested — per-position uses group directly
        assert case.input_dtypes == (("float16", "float32"), "int32")

    def test_scalar_group_single_broadcast(self):
        """('float16',) at TensorList(2) → broadcast to ('float16','float16')."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=(("float16",), "float32"),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float16"), "float32")

    def test_scalar_bare_value_broadcast(self):
        """'float16' at TensorList(2) → broadcast to ('float16','float16')."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_dtypes=("float16", "float32"),
        )
        _validate(case)
        assert case.input_dtypes == (("float16", "float16"), "float32")

    def test_range_group_matches_dist(self):
        """Range group at TensorList position with matching len → use as-is."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            precision_tolerances=(((0.001, 0.002), (0.003, 0.004)),),
        )
        _validate(case)
        assert case.precision_tolerances == (((0.001, 0.002), (0.003, 0.004)),)

    def test_range_group_single_broadcast(self):
        """((0.004,0.004),) at TensorList(2) → broadcast."""
        case = _make_testcase(
            output_shapes=(((8,), (8,)),),
            precision_tolerances=(((0.004, 0.004),),),
        )
        _validate(case)
        assert case.precision_tolerances == (((0.004, 0.004), (0.004, 0.004)),)

    def test_range_group_mismatch_rejected(self):
        """Range group with wrong len at TensorList position → REJECT."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
        )
        case.input_data_ranges = (((0, 1), (0, 1), (0, 1)), (0, 5))
        case._normalize_field_by_dist(
            "input_data_ranges", (2, 0), TestcaseOp._is_range_group)
        assert case.is_valid is False
        assert case.fail_reason == "CASE_FIELD_AMBIGUOUS"


class TestNormalizeFieldGuardChecks:
    """Entry guard tests."""

    def test_not_is_valid_returns_early(self):
        """If is_valid is False, normalize does nothing."""
        case = _make_testcase()
        case.is_valid = False
        case.input_dtypes = ("float16",)
        original = case.input_dtypes
        case._normalize_field_by_dist(
            "input_dtypes", (2, 0), TestcaseOp._is_scalar_group)
        # Should not modify — early return
        assert case.input_dtypes == original

    def test_empty_field_returns_early(self):
        """Empty field → skip normalize."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
            input_formats=(),
        )
        _validate(case)
        assert case.input_formats == ()

    def test_none_field_returns_early(self):
        """None field → skip normalize."""
        case = _make_testcase()
        case.precision_tolerances = None
        _validate(case)
        assert case.precision_tolerances is None


class TestNormalizeFieldCallerPrewrap:
    """Caller is responsible for wrapping bare scalar values."""

    def test_scalar_bare_string_wrapped_by_caller(self):
        """Bare 'ND' must be wrapped to ('ND',) by caller before calling."""
        case = _make_testcase(
            input_shapes=(((3, 4), (5, 4)), (8,)),
        )
        # Simulate caller wrapping
        case.input_dtypes = ("float16",)
        case._normalize_field_by_dist(
            "input_dtypes", (2, 0), TestcaseOp._is_scalar_group)
        assert case.input_dtypes == (("float16", "float16"), "float16")