"""Unit tests for SceneEntityCfg resolution logic."""

from dataclasses import dataclass

import pytest

from mjlab.managers.scene_entity_config import SceneEntityCfg


@dataclass
class _FakeEntity:
  name: str

  joint_names: tuple[str, ...]
  body_names: tuple[str, ...]
  geom_names: tuple[str, ...]
  site_names: tuple[str, ...]

  @property
  def num_joints(self) -> int:
    return len(self.joint_names)

  @property
  def num_bodies(self) -> int:
    return len(self.body_names)

  @property
  def num_geoms(self) -> int:
    return len(self.geom_names)

  @property
  def num_sites(self) -> int:
    return len(self.site_names)

  # find_* helpers return (ids, names) similar to Entity API.
  def _find(
    self,
    query_names: tuple[str, ...],
    pool: tuple[str, ...],
    preserve_order: bool,
  ) -> tuple[list[int], list[str]]:
    # Treat query as exact names (no regex) to keep tests minimal.
    indices = [list(pool).index(n) for n in query_names]
    if not preserve_order:
      indices.sort()
    names = [list(pool)[i] for i in indices]
    return indices, names

  def find_joints(self, query_names: tuple[str, ...], preserve_order: bool = False):
    return self._find(query_names, self.joint_names, preserve_order)

  def find_bodies(self, query_names: tuple[str, ...], preserve_order: bool = False):
    return self._find(query_names, self.body_names, preserve_order)

  def find_geoms(self, query_names: tuple[str, ...], preserve_order: bool = False):
    return self._find(query_names, self.geom_names, preserve_order)

  def find_sites(self, query_names: tuple[str, ...], preserve_order: bool = False):
    return self._find(query_names, self.site_names, preserve_order)


@pytest.fixture
def fake_entity() -> _FakeEntity:
  names = ("a", "b", "c")
  return _FakeEntity(
    name="robot",
    joint_names=names,
    body_names=names,
    geom_names=names,
    site_names=names,
  )


@pytest.fixture
def fake_scene(fake_entity: _FakeEntity):
  # Minimal scene-like mapping.
  return {fake_entity.name: fake_entity}


@pytest.mark.parametrize(
  "field_names",
  [
    ("joint_names", "joint_ids"),
    ("body_names", "body_ids"),
    ("geom_names", "geom_ids"),
    ("site_names", "site_ids"),
  ],
)
def test_names_to_ids_sets_slice_when_all(fake_scene, fake_entity, field_names):
  """Providing full set of names resolves ids and collapses to slice(None)."""
  names_attr, ids_attr = field_names

  cfg = SceneEntityCfg(name=fake_entity.name)
  setattr(cfg, names_attr, getattr(fake_entity, names_attr))

  cfg.resolve(fake_scene)

  ids_value = getattr(cfg, ids_attr)
  assert isinstance(ids_value, slice), f"{ids_attr} should collapse to slice(None)"
  assert ids_value == slice(None)


@pytest.mark.parametrize(
  "field_names,ids",
  [
    (("joint_names", "joint_ids"), [0, 2]),
    (("body_names", "body_ids"), [1]),
    (("geom_names", "geom_ids"), [2, 0]),
    (("site_names", "site_ids"), [1, 2]),
  ],
)
def test_ids_to_names_resolves_names(fake_scene, fake_entity, field_names, ids):
  """Providing explicit ids populates the corresponding names list."""
  names_attr, ids_attr = field_names

  cfg = SceneEntityCfg(name=fake_entity.name)
  setattr(cfg, ids_attr, ids.copy())

  cfg.resolve(fake_scene)

  names_value = getattr(cfg, names_attr)
  expected = [getattr(fake_entity, names_attr)[i] for i in ids]
  assert names_value == expected


@pytest.mark.parametrize(
  "field_names,provided_names,provided_ids",
  [
    (("joint_names", "joint_ids"), ["a"], [1]),
    (("body_names", "body_ids"), ["b"], [2]),
    (("geom_names", "geom_ids"), ["c"], [0]),
    (("site_names", "site_ids"), ["a"], [2]),
  ],
)
def test_inconsistent_names_and_ids_raise(
  fake_scene, field_names, provided_names, provided_ids
):
  """When both names and ids are provided but disagree, a ValueError is raised."""
  names_attr, ids_attr = field_names

  cfg = SceneEntityCfg(name="robot")
  setattr(cfg, names_attr, provided_names.copy())
  setattr(cfg, ids_attr, provided_ids.copy())  # Must be list to trigger check.

  with pytest.raises(ValueError):
    cfg.resolve(fake_scene)


def test_tuple_names_with_consistent_ids(fake_scene):
  """Tuple names with matching IDs should validate without error."""
  cfg = SceneEntityCfg(name="robot")
  cfg.joint_names = ("a", "b")
  cfg.joint_ids = [0, 1]

  cfg.resolve(fake_scene)

  assert cfg.joint_names == ["a", "b"]
  assert cfg.joint_ids == [0, 1]


@pytest.mark.parametrize(
  "field_names",
  [
    ("joint_names", "joint_ids"),
    ("body_names", "body_ids"),
    ("geom_names", "geom_ids"),
    ("site_names", "site_ids"),
  ],
)
def test_names_reordered_to_match_ids_when_not_preserving_order(
  fake_scene, field_names
):
  """With preserve_order=False, names are reordered to internal order."""
  names_attr, ids_attr = field_names

  # Give a subset in reverse of internal order.
  cfg = SceneEntityCfg(name="robot")
  setattr(cfg, names_attr, ["c", "a"])

  cfg.resolve(fake_scene)

  # IDs and names must both be in internal order: a=0, c=2.
  assert getattr(cfg, ids_attr) == [0, 2]
  assert getattr(cfg, names_attr) == ["a", "c"]


@pytest.mark.parametrize(
  "field_names",
  [
    ("joint_names", "joint_ids"),
    ("body_names", "body_ids"),
    ("geom_names", "geom_ids"),
    ("site_names", "site_ids"),
  ],
)
def test_names_preserve_user_order_when_preserving_order(fake_scene, field_names):
  """With preserve_order=True, names stay in the user-specified order."""
  names_attr, ids_attr = field_names

  cfg = SceneEntityCfg(name="robot", preserve_order=True)
  setattr(cfg, names_attr, ["c", "a"])

  cfg.resolve(fake_scene)

  # User order preserved: c=2, a=0.
  assert getattr(cfg, ids_attr) == [2, 0]
  assert getattr(cfg, names_attr) == ["c", "a"]
