"""Tests for unxt config loading and context behavior."""

import json
import subprocess
import sys
import textwrap
import threading
import time
from pathlib import Path

import pytest
from traitlets.config import Config

import unxt as u
from unxt._src.config import (
    _find_pyproject,
    _load_toml_config_from_pyproject,
    _walk_toml_config,
)

# =============================================================================
# Unit Tests for Internal Helper Functions


def test_walk_toml_config_simple() -> None:
    """Test _walk_toml_config with simple nested structure."""
    data = {
        "quantity": {
            "repr": {"short_arrays": "compact", "use_short_name": True},
            "str": {"named_unit": False},
        }
    }

    result = _walk_toml_config(data)
    assert "QuantityReprConfig" in result
    assert "QuantityStrConfig" in result
    assert result["QuantityReprConfig"]["short_arrays"] == "compact"
    assert result["QuantityReprConfig"]["use_short_name"] is True
    assert result["QuantityStrConfig"]["named_unit"] is False


def test_walk_toml_config_empty() -> None:
    """Test _walk_toml_config with empty dict."""
    result = _walk_toml_config({})
    assert result == {}


def test_walk_toml_config_unknown_paths() -> None:
    """Test _walk_toml_config ignores unknown paths."""
    data = {
        "unknown": {"section": {"key": "value"}},
        "quantity": {
            "repr": {"use_short_name": True},
            "unknown_subsection": {"key": "ignored"},
        },
    }

    result = _walk_toml_config(data)
    assert "QuantityReprConfig" in result
    assert result["QuantityReprConfig"]["use_short_name"] is True
    # Unknown paths should not create config entries
    assert len(result) == 1


def test_walk_toml_config_non_dict_values() -> None:
    """Test _walk_toml_config handles non-dict values gracefully."""
    data = {
        "quantity": {
            "repr": "not-a-dict",  # Invalid: should be dict
            "str": {"named_unit": False},  # Valid
        }
    }

    result = _walk_toml_config(data)
    # Invalid entries should be skipped
    assert "QuantityReprConfig" not in result
    assert "QuantityStrConfig" in result


def test_load_toml_config_from_pyproject(tmp_path: Path) -> None:
    """Test _load_toml_config_from_pyproject function directly."""
    pyproject = tmp_path / "pyproject.toml"
    pyproject.write_text(
        """\
[tool.unxt.quantity.repr]
short_arrays = "compact"
use_short_name = true
""",
        encoding="utf-8",
    )

    config = _load_toml_config_from_pyproject(pyproject)
    assert "QuantityReprConfig" in config
    assert config["QuantityReprConfig"]["short_arrays"] == "compact"
    assert config["QuantityReprConfig"]["use_short_name"] is True


def test_load_toml_config_no_tool_section(tmp_path: Path) -> None:
    """Test _load_toml_config_from_pyproject with no [tool] section."""
    pyproject = tmp_path / "pyproject.toml"
    pyproject.write_text("[project]\nname = 'test'\n", encoding="utf-8")

    config = _load_toml_config_from_pyproject(pyproject)
    assert len(config) == 0


def test_load_toml_config_no_unxt_section(tmp_path: Path) -> None:
    """Test _load_toml_config_from_pyproject with no [tool.unxt] section."""
    pyproject = tmp_path / "pyproject.toml"
    pyproject.write_text(
        "[tool.other]\nenabled = true\n",
        encoding="utf-8",
    )

    config = _load_toml_config_from_pyproject(pyproject)
    assert len(config) == 0


def test_find_pyproject_in_cwd(tmp_path: Path) -> None:
    """Test _find_pyproject finds pyproject.toml in current directory."""
    pyproject = tmp_path / "pyproject.toml"
    pyproject.write_text("[project]\n", encoding="utf-8")

    result = _find_pyproject(tmp_path)
    assert result == pyproject


def test_find_pyproject_in_parent(tmp_path: Path) -> None:
    """Test _find_pyproject searches parent directories."""
    pyproject = tmp_path / "pyproject.toml"
    pyproject.write_text("[project]\n", encoding="utf-8")

    subdir = tmp_path / "subdir" / "nested"
    subdir.mkdir(parents=True)

    result = _find_pyproject(subdir)
    assert result == pyproject


def test_find_pyproject_not_found(tmp_path: Path) -> None:
    """Test _find_pyproject returns None when no pyproject.toml found."""
    # Create a directory with no pyproject.toml
    subdir = tmp_path / "no_project"
    subdir.mkdir()

    result = _find_pyproject(subdir)
    assert result is None


def test_find_pyproject_prefers_nearest(tmp_path: Path) -> None:
    """Test _find_pyproject returns the nearest pyproject.toml."""
    # Create pyproject.toml in root
    root_pyproject = tmp_path / "pyproject.toml"
    root_pyproject.write_text("[project]\nname = 'root'\n", encoding="utf-8")

    # Create nested directory with its own pyproject.toml
    nested = tmp_path / "subdir" / "nested"
    nested.mkdir(parents=True)
    nested_pyproject = nested / "pyproject.toml"
    nested_pyproject.write_text("[project]\nname = 'nested'\n", encoding="utf-8")

    # Should find the nearest one
    result = _find_pyproject(nested)
    assert result == nested_pyproject


# =============================================================================
# Auto-loading Tests (run in subprocess)


def _run_isolated_import(
    cwd: Path, extra_code: str = "", pre_import_code: str = ""
) -> str:
    """Import unxt in a subprocess and print config values as JSON."""
    code = "\n".join(
        [
            textwrap.dedent(pre_import_code).strip(),
            "import json",
            "import unxt as u",
            textwrap.dedent(extra_code).strip(),
            textwrap.dedent(
                """
                payload = {
                    "short_arrays": u.config.quantity_repr.short_arrays,
                    "use_short_name": u.config.quantity_repr.use_short_name,
                    "named_unit": u.config.quantity_repr.named_unit,
                    "include_params": u.config.quantity_repr.include_params,
                }
                print(json.dumps(payload))
                """
            ).strip(),
        ]
    )
    result = subprocess.run(  # noqa: S603
        [sys.executable, "-c", code],
        cwd=cwd,
        check=True,
        capture_output=True,
        text=True,
    )
    return result.stdout.strip()


def test_auto_load_toml_from_pyproject_on_import(tmp_path: Path) -> None:
    """Auto-load TOML config from nearest pyproject.toml when importing unxt."""
    project = tmp_path / "demo_project"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

[tool.unxt.quantity.repr]
short_arrays = "compact"
use_short_name = true
named_unit = false
include_params = true
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    assert payload["short_arrays"] == "compact"
    assert payload["use_short_name"] is True
    assert payload["named_unit"] is False
    assert payload["include_params"] is True


def test_auto_load_uses_nearest_pyproject(tmp_path: Path) -> None:
    """Auto-loading should prefer the closest pyproject.toml in parent search."""
    root = tmp_path / "workspace"
    pkg = root / "packages" / "pkg_a"
    pkg.mkdir(parents=True)

    _ = (root / "pyproject.toml").write_text(
        """\
[tool.unxt.quantity.repr]
short_arrays = false
use_short_name = false
""",
        encoding="utf-8",
    )
    _ = (pkg / "pyproject.toml").write_text(
        """\
[tool.unxt.quantity.repr]
short_arrays = "compact"
use_short_name = true
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(pkg))
    assert payload["short_arrays"] == "compact"
    assert payload["use_short_name"] is True


def test_auto_load_ignores_pyproject_without_tool_unxt(tmp_path: Path) -> None:
    """Import should keep defaults if pyproject.toml has no [tool.unxt] section."""
    project = tmp_path / "no_unxt_config"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

[tool.other]
enabled = true
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    assert payload["short_arrays"] is False
    assert payload["use_short_name"] is False
    assert payload["named_unit"] is True
    assert payload["include_params"] is False


def test_auto_load_ignores_invalid_toml_entries(tmp_path: Path) -> None:
    """Malformed TOML entries should not fail import and valid keys still apply."""
    project = tmp_path / "invalid_unxt_config"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

[tool.unxt.quantity.repr]
use_short_name = true
indent = "not-an-int"
typo_option = true

[tool.unxt.quantity.str]
indent = "not-an-int"
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    # Valid key should be applied.
    assert payload["use_short_name"] is True
    # Invalid values/keys should be ignored without breaking import.
    assert payload["short_arrays"] is False
    assert payload["named_unit"] is True
    assert payload["include_params"] is False


def test_auto_load_dotted_key_syntax(tmp_path: Path) -> None:
    """Auto-load TOML config using dotted key syntax (quantity.repr.use_short_name)."""
    project = tmp_path / "dotted_syntax"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

[tool.unxt]
quantity.repr.use_short_name = true
quantity.repr.short_arrays = "compact"
quantity.str.use_short_name = true
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    assert payload["use_short_name"] is True
    assert payload["short_arrays"] == "compact"


def test_auto_load_mixed_dotted_and_nested_syntax(tmp_path: Path) -> None:
    """Auto-load supports mixing dotted keys and nested sections."""
    project = tmp_path / "mixed_syntax"
    project.mkdir()
    # Note: TOML doesn't allow declaring the same table twice, so we use
    # dotted keys for one section and nested tables for another
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

# Dotted key syntax for repr config
[tool.unxt]
quantity.repr.use_short_name = true
quantity.repr.short_arrays = "compact"

# Nested section syntax for str config
[tool.unxt.quantity.str]
named_unit = false
use_short_name = true
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    # Both syntaxes should work together (for different config sections)
    assert payload["use_short_name"] is True
    assert payload["short_arrays"] == "compact"


def test_auto_load_empty_tool_unxt_section(tmp_path: Path) -> None:
    """Empty [tool.unxt] section should not affect defaults."""
    project = tmp_path / "empty_unxt"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

[tool.unxt]
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    # Should use all defaults
    assert payload["short_arrays"] is False
    assert payload["use_short_name"] is False
    assert payload["named_unit"] is True
    assert payload["include_params"] is False


def test_auto_load_unknown_toml_sections_ignored(tmp_path: Path) -> None:
    """Unknown sections under [tool.unxt] should be silently ignored."""
    project = tmp_path / "unknown_sections"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[tool.unxt.quantity.repr]
use_short_name = true

[tool.unxt.future_feature]
enabled = true
some_value = 42

[tool.unxt.another.unknown.path]
value = "ignored"
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    # Known config should work
    assert payload["use_short_name"] is True
    # Unknown sections should be ignored (no error)


def test_auto_load_malformed_toml_file(tmp_path: Path) -> None:
    """Malformed TOML should not crash import (silently ignored)."""
    project = tmp_path / "malformed_toml"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
this is malformed TOML!!!
""",
        encoding="utf-8",
    )

    # Should not raise, just use defaults
    payload = json.loads(_run_isolated_import(project))
    assert payload["short_arrays"] is False
    assert payload["use_short_name"] is False


def test_auto_load_no_pyproject_file(tmp_path: Path) -> None:
    """No pyproject.toml should use defaults without error."""
    project = tmp_path / "no_pyproject"
    project.mkdir()
    # No pyproject.toml file created

    payload = json.loads(_run_isolated_import(project))
    # Should use all defaults
    assert payload["short_arrays"] is False
    assert payload["use_short_name"] is False
    assert payload["named_unit"] is True
    assert payload["include_params"] is False


def test_auto_load_non_dict_tool_section(tmp_path: Path) -> None:
    """Non-dict [tool] section should be ignored without error."""
    project = tmp_path / "non_dict_tool"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

tool = "not-a-dict"
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    assert payload["short_arrays"] is False


def test_auto_load_non_dict_unxt_section(tmp_path: Path) -> None:
    """Non-dict [tool.unxt] section should be ignored without error."""
    project = tmp_path / "non_dict_unxt"
    project.mkdir()
    _ = (project / "pyproject.toml").write_text(
        """\
[project]
name = "demo-project"
version = "0.1.0"

[tool]
unxt = "not-a-dict"
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    assert payload["short_arrays"] is False


# =============================================================================
# Context Manager Tests


def test_config_context_manager_basic() -> None:
    """Test basic context manager functionality."""
    # Get original values
    original_short_arrays = u.config.quantity_repr.short_arrays
    original_use_short_name = u.config.quantity_repr.use_short_name

    try:
        # Inside context, values should be overridden
        with u.config.override(
            quantity_repr__short_arrays="compact", quantity_repr__use_short_name=True
        ):
            assert u.config.quantity_repr.short_arrays == "compact"
            assert u.config.quantity_repr.use_short_name is True

        # After exiting context, values should be restored
        assert u.config.quantity_repr.short_arrays == original_short_arrays
        assert u.config.quantity_repr.use_short_name == original_use_short_name
    finally:
        # Ensure cleanup even if test fails
        u.config.quantity_repr.short_arrays = original_short_arrays
        u.config.quantity_repr.use_short_name = original_use_short_name


def test_config_context_manager_nested() -> None:
    """Test nested context managers."""
    original_short_arrays = u.config.quantity_repr.short_arrays
    original_use_short_name = u.config.quantity_repr.use_short_name

    try:
        # First level
        with u.config.override(quantity_repr__short_arrays="compact"):
            assert u.config.quantity_repr.short_arrays == "compact"
            assert u.config.quantity_repr.use_short_name == original_use_short_name

            # Second level (nested)
            with u.config.override(quantity_repr__use_short_name=True):
                assert u.config.quantity_repr.short_arrays == "compact"
                assert u.config.quantity_repr.use_short_name is True

                # Third level (override previous)
                with u.config.override(quantity_repr__short_arrays=True):
                    assert u.config.quantity_repr.short_arrays is True
                    assert u.config.quantity_repr.use_short_name is True

                # Back to second level
                assert u.config.quantity_repr.short_arrays == "compact"
                assert u.config.quantity_repr.use_short_name is True

            # Back to first level
            assert u.config.quantity_repr.short_arrays == "compact"
            assert u.config.quantity_repr.use_short_name == original_use_short_name

        # Back to original
        assert u.config.quantity_repr.short_arrays == original_short_arrays
        assert u.config.quantity_repr.use_short_name == original_use_short_name
    finally:
        u.config.quantity_repr.short_arrays = original_short_arrays
        u.config.quantity_repr.use_short_name = original_use_short_name


def test_config_context_manager_thread_local() -> None:
    """Test that nested config override() is thread-safe."""
    # The nested config override() uses thread-local storage for thread safety
    original_short_arrays = u.config.quantity_repr.short_arrays
    results = {}

    def thread_function(name: str) -> None:
        """Function to run in a separate thread."""
        # Check initial value
        results[f"{name}_before"] = u.config.quantity_repr.short_arrays

        # Set thread-local override using nested config's override method
        with u.config.quantity_repr.override(
            short_arrays="compact" if name == "thread1" else True
        ):
            results[f"{name}_inside"] = u.config.quantity_repr.short_arrays
            time.sleep(0.01)
            results[f"{name}_still_inside"] = u.config.quantity_repr.short_arrays

        # After exiting
        results[f"{name}_after"] = u.config.quantity_repr.short_arrays

    try:
        # Start two threads with different overrides
        thread1 = threading.Thread(target=thread_function, args=("thread1",))
        thread2 = threading.Thread(target=thread_function, args=("thread2",))

        thread1.start()
        thread2.start()

        thread1.join()
        thread2.join()

        # Verify thread1 results - should be thread-local
        assert results["thread1_before"] == original_short_arrays
        assert results["thread1_inside"] == "compact"
        assert results["thread1_still_inside"] == "compact"
        assert results["thread1_after"] == original_short_arrays

        # Verify thread2 results - should be thread-local
        assert results["thread2_before"] == original_short_arrays
        assert results["thread2_inside"] is True
        assert results["thread2_still_inside"] is True
        assert results["thread2_after"] == original_short_arrays

        # Main thread should be unchanged
        assert u.config.quantity_repr.short_arrays == original_short_arrays
    finally:
        u.config.quantity_repr.short_arrays = original_short_arrays


def test_root_config_context_manager_thread_local() -> None:
    """Test that root config override() delegates thread-local behavior."""
    original_repr_short_arrays = u.config.quantity_repr.short_arrays
    original_str_named_unit = u.config.quantity_str.named_unit
    results = {}

    def thread_function(name: str) -> None:
        """Function to run in a separate thread."""
        # Check initial values
        results[f"{name}_repr_before"] = u.config.quantity_repr.short_arrays
        results[f"{name}_str_before"] = u.config.quantity_str.named_unit

        # Set thread-local override using root config override method
        with u.config.override(
            quantity_repr__short_arrays=("compact" if name == "thread1" else True),
            quantity_str__named_unit=(name == "thread1"),
        ):
            results[f"{name}_repr_inside"] = u.config.quantity_repr.short_arrays
            results[f"{name}_str_inside"] = u.config.quantity_str.named_unit
            time.sleep(0.01)
            results[f"{name}_repr_still_inside"] = u.config.quantity_repr.short_arrays
            results[f"{name}_str_still_inside"] = u.config.quantity_str.named_unit

        # After exiting
        results[f"{name}_repr_after"] = u.config.quantity_repr.short_arrays
        results[f"{name}_str_after"] = u.config.quantity_str.named_unit

    try:
        # Start two threads with different overrides
        thread1 = threading.Thread(target=thread_function, args=("thread1",))
        thread2 = threading.Thread(target=thread_function, args=("thread2",))

        thread1.start()
        thread2.start()

        thread1.join()
        thread2.join()

        # Verify thread1 results - should be thread-local
        assert results["thread1_repr_before"] == original_repr_short_arrays
        assert results["thread1_repr_inside"] == "compact"
        assert results["thread1_repr_still_inside"] == "compact"
        assert results["thread1_repr_after"] == original_repr_short_arrays

        assert results["thread1_str_before"] == original_str_named_unit
        assert results["thread1_str_inside"] is True
        assert results["thread1_str_still_inside"] is True
        assert results["thread1_str_after"] == original_str_named_unit

        # Verify thread2 results - should be thread-local
        assert results["thread2_repr_before"] == original_repr_short_arrays
        assert results["thread2_repr_inside"] is True
        assert results["thread2_repr_still_inside"] is True
        assert results["thread2_repr_after"] == original_repr_short_arrays

        assert results["thread2_str_before"] == original_str_named_unit
        assert results["thread2_str_inside"] is False
        assert results["thread2_str_still_inside"] is False
        assert results["thread2_str_after"] == original_str_named_unit

        # Main thread should be unchanged
        assert u.config.quantity_repr.short_arrays == original_repr_short_arrays
        assert u.config.quantity_str.named_unit == original_str_named_unit
    finally:
        u.config.quantity_repr.short_arrays = original_repr_short_arrays
        u.config.quantity_str.named_unit = original_str_named_unit


def test_config_context_manager_partial_override() -> None:
    """Test that only specified values are overridden."""
    original_short_arrays = u.config.quantity_repr.short_arrays
    original_use_short_name = u.config.quantity_repr.use_short_name
    original_named_unit = u.config.quantity_repr.named_unit

    try:
        # Override only short_arrays
        with u.config.override(quantity_repr__short_arrays="compact"):
            assert u.config.quantity_repr.short_arrays == "compact"
            # Other values should remain unchanged
            assert u.config.quantity_repr.use_short_name == original_use_short_name
            assert u.config.quantity_repr.named_unit == original_named_unit

        # All values restored
        assert u.config.quantity_repr.short_arrays == original_short_arrays
        assert u.config.quantity_repr.use_short_name == original_use_short_name
        assert u.config.quantity_repr.named_unit == original_named_unit
    finally:
        u.config.quantity_repr.short_arrays = original_short_arrays
        u.config.quantity_repr.use_short_name = original_use_short_name
        u.config.quantity_repr.named_unit = original_named_unit


def test_config_context_manager_with_quantity() -> None:
    """Test context manager actually affects Quantity repr."""
    original_short_arrays = u.config.quantity_repr.short_arrays
    original_use_short_name = u.config.quantity_repr.use_short_name

    try:
        # Explicitly set config to known non-compact state
        u.config.quantity_repr.short_arrays = False
        u.config.quantity_repr.use_short_name = False

        q = u.Quantity([1, 2, 3], "m")

        # Default repr (with short_arrays=False, use_short_name=False)
        default_repr = repr(q)
        assert "Quantity(" in default_repr  # Full name, not short
        assert "Array" in default_repr  # Not compact

        # With compact arrays and short name
        with u.config.override(
            quantity_repr__short_arrays="compact", quantity_repr__use_short_name=True
        ):
            compact_repr = repr(q)
            assert "Q(" in compact_repr  # Short name
            assert "[1, 2, 3]" in compact_repr  # Compact array
            assert "Array" not in compact_repr  # Not the Array wrapper
            assert compact_repr != default_repr

        # Back to default
        restored_repr = repr(q)
        assert restored_repr == default_repr
    finally:
        u.config.quantity_repr.short_arrays = original_short_arrays
        u.config.quantity_repr.use_short_name = original_use_short_name


def test_nested_config_override() -> None:
    """Test that nested config objects support override() method directly."""
    original_short_arrays = u.config.quantity_repr.short_arrays
    original_use_short_name = u.config.quantity_repr.use_short_name

    try:
        # Test override on nested config directly
        with u.config.quantity_repr.override(
            short_arrays="compact", use_short_name=True
        ):
            assert u.config.quantity_repr.short_arrays == "compact"
            assert u.config.quantity_repr.use_short_name is True

        # After exiting context, values should be restored
        assert u.config.quantity_repr.short_arrays == original_short_arrays
        assert u.config.quantity_repr.use_short_name == original_use_short_name
    finally:
        u.config.quantity_repr.short_arrays = original_short_arrays
        u.config.quantity_repr.use_short_name = original_use_short_name


def test_nested_str_config_override() -> None:
    """Test that QuantityStrConfig override() is applied and restored."""
    original_short_arrays = u.config.quantity_str.short_arrays
    original_named_unit = u.config.quantity_str.named_unit

    try:
        with u.config.quantity_str.override(short_arrays=True, named_unit=False):
            assert u.config.quantity_str.short_arrays is True
            assert u.config.quantity_str.named_unit is False

        assert u.config.quantity_str.short_arrays == original_short_arrays
        assert u.config.quantity_str.named_unit == original_named_unit
    finally:
        u.config.quantity_str.short_arrays = original_short_arrays
        u.config.quantity_str.named_unit = original_named_unit


def test_nested_repr_config_override_invalid_key() -> None:
    """QuantityReprConfig.override() rejects unknown keyword options."""
    with pytest.raises(ValueError, match="Unknown QuantityReprConfig override option"):  # noqa: SIM117
        with u.config.quantity_repr.override(short_arrayz="compact"):
            pass


def test_nested_repr_config_override_invalid_value() -> None:
    """QuantityReprConfig.override() rejects invalid values immediately."""
    with pytest.raises(  # noqa: SIM117
        ValueError, match="Invalid value for QuantityReprConfig override option"
    ):
        with u.config.quantity_repr.override(indent="not-an-int"):
            pass


def test_nested_repr_config_override_with_config_object() -> None:
    """Test QuantityReprConfig.override() with a traitlets Config object."""
    original_short_arrays = u.config.quantity_repr.short_arrays
    original_use_short_name = u.config.quantity_repr.use_short_name

    try:
        cfg = Config()
        cfg.QuantityReprConfig.short_arrays = "compact"
        cfg.QuantityReprConfig.use_short_name = True

        with u.config.quantity_repr.override(cfg):
            assert u.config.quantity_repr.short_arrays == "compact"
            assert u.config.quantity_repr.use_short_name is True

        # Restored after exit
        assert u.config.quantity_repr.short_arrays == original_short_arrays
        assert u.config.quantity_repr.use_short_name == original_use_short_name
    finally:
        u.config.quantity_repr.short_arrays = original_short_arrays
        u.config.quantity_repr.use_short_name = original_use_short_name


def test_nested_repr_config_override_with_config_and_kwargs_raises() -> None:
    """QuantityReprConfig.override() raises if both cfg and kwargs provided."""
    cfg = Config()
    cfg.QuantityReprConfig.short_arrays = "compact"

    with pytest.raises(  # noqa: SIM117
        ValueError, match="Cannot specify both cfg and keyword arguments"
    ):
        with u.config.quantity_repr.override(cfg, short_arrays=True):
            pass


def test_nested_str_config_override_invalid_key() -> None:
    """QuantityStrConfig.override() rejects unknown keyword options."""
    with pytest.raises(ValueError, match="Unknown QuantityStrConfig override option"):  # noqa: SIM117
        with u.config.quantity_str.override(short_arrayz=True):
            pass


def test_nested_str_config_override_invalid_value() -> None:
    """QuantityStrConfig.override() rejects invalid values immediately."""
    with pytest.raises(  # noqa: SIM117
        ValueError, match="Invalid value for QuantityStrConfig override option"
    ):
        with u.config.quantity_str.override(indent="not-an-int"):
            pass


def test_root_override_invalid_config_section() -> None:
    """UnxtConfig.override() rejects unknown config sections."""
    with pytest.raises(ValueError, match="Unknown config section"):  # noqa: SIM117
        with u.config.override(quantity_repp__short_arrays="compact"):
            pass


def test_root_override_invalid_config_option() -> None:
    """UnxtConfig.override() rejects unknown options in known sections."""
    with pytest.raises(ValueError, match="Unknown option 'short_arrayz'"):  # noqa: SIM117
        with u.config.override(quantity_repr__short_arrayz="compact"):
            pass


def test_root_override_invalid_config_value() -> None:
    """UnxtConfig.override() surfaces nested invalid values immediately."""
    with pytest.raises(  # noqa: SIM117
        ValueError, match="Invalid value for QuantityReprConfig override option"
    ):
        with u.config.override(quantity_repr__indent="not-an-int"):
            pass


def test_config_not_callable_for_context_override() -> None:
    """Config overrides must use the explicit override() method."""
    with pytest.raises(TypeError):
        _ = u.config(short_arrays="compact")


def test_root_override_requires_double_underscore() -> None:
    """UnxtConfig.override() requires double-underscore notation."""
    with pytest.raises(ValueError, match="must use double-underscore notation"):  # noqa: SIM117
        with u.config.override(short_arrays="compact"):
            pass


def test_auto_load_handles_oserror_gracefully(tmp_path: Path) -> None:
    """Test that OSError during file read is handled gracefully."""
    project = tmp_path / "oserror_project"
    project.mkdir()
    pyproject = project / "pyproject.toml"
    pyproject.write_text(
        "[tool.unxt.quantity.repr]\nuse_short_name = true\n", encoding="utf-8"
    )

    # Patch pathlib.Path.open before importing unxt in the subprocess so
    # reading pyproject.toml fails during auto-load.
    pre_import_code = """
import pathlib

_orig_open = pathlib.Path.open


def _open_raises_for_pyproject(self, *args, **kwargs):
    if self.name == "pyproject.toml":
        raise OSError("simulated read failure")
    return _orig_open(self, *args, **kwargs)


pathlib.Path.open = _open_raises_for_pyproject
"""

    payload = json.loads(_run_isolated_import(project, pre_import_code=pre_import_code))

    # Import should not fail, and defaults should be preserved.
    assert payload["short_arrays"] is False
    assert payload["use_short_name"] is False
    assert payload["named_unit"] is True
    assert payload["include_params"] is False


def test_auto_load_handles_keyerror_in_toml(tmp_path: Path) -> None:
    """Test that KeyError during TOML processing doesn't crash import."""
    # Create a valid pyproject.toml that could produce KeyError during processing
    project = tmp_path / "keyerror_project"
    project.mkdir()
    # This is a valid TOML that our code can parse without KeyError,
    # but the error handler is there for robustness
    pyproject = project / "pyproject.toml"
    pyproject.write_text(
        """\
[tool.unxt.quantity.repr]
use_short_name = true
""",
        encoding="utf-8",
    )

    payload = json.loads(_run_isolated_import(project))
    assert payload["use_short_name"] is True


def test_nested_str_config_override_with_config_object() -> None:
    """Test QuantityStrConfig.override() with a traitlets Config object."""
    original_short_arrays = u.config.quantity_str.short_arrays
    original_named_unit = u.config.quantity_str.named_unit

    try:
        cfg = Config()
        cfg.QuantityStrConfig.short_arrays = True
        cfg.QuantityStrConfig.named_unit = False

        with u.config.quantity_str.override(cfg):
            assert u.config.quantity_str.short_arrays is True
            assert u.config.quantity_str.named_unit is False

        # Restored after exit
        assert u.config.quantity_str.short_arrays == original_short_arrays
        assert u.config.quantity_str.named_unit == original_named_unit
    finally:
        u.config.quantity_str.short_arrays = original_short_arrays
        u.config.quantity_str.named_unit = original_named_unit


def test_nested_str_config_override_with_config_and_kwargs_raises() -> None:
    """QuantityStrConfig.override() raises if both cfg and kwargs provided."""
    cfg = Config()
    cfg.QuantityStrConfig.short_arrays = True

    with pytest.raises(  # noqa: SIM117
        ValueError, match="Cannot specify both cfg and keyword arguments"
    ):
        with u.config.quantity_str.override(cfg, short_arrays=False):
            pass


# =============================================================================
# Integration Test: Mock Package


def test_auto_load_config_from_package_pyproject_toml(tmp_path: Path) -> None:
    """Test auto-loading config from a package's pyproject.toml (integration test).

    This simulates a real-world scenario where a uv-based package has its
    unxt configuration in pyproject.toml alongside project metadata.
    """
    # Create a mock package structure
    package_root = tmp_path / "my_physics_project"
    package_root.mkdir()

    src_dir = package_root / "src" / "my_physics_project"
    src_dir.mkdir(parents=True)

    # Create a minimal package with __init__.py
    (src_dir / "__init__.py").write_text(
        '"""My physics analysis package."""\n\n__version__ = "0.1.0"\n',
        encoding="utf-8",
    )

    # Create a pyproject.toml with both project metadata and unxt config
    pyproject_content = """\
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-physics-project"
version = "0.1.0"
description = "A physics analysis package using unxt"
requires-python = ">=3.11"
dependencies = [
    "unxt",
    "jax",
]

[tool.uv]
dev-dependencies = [
    "pytest>=8.0",
]

[tool.unxt.quantity.repr]
# Configure unxt for compact notebook output
short_arrays = "compact"
use_short_name = true
named_unit = false
include_params = true
"""
    pyproject_file = package_root / "pyproject.toml"
    pyproject_file.write_text(pyproject_content, encoding="utf-8")

    # Create a README
    readme = package_root / "README.md"
    readme.write_text(
        "# My Physics Project\n\nUsing unxt with custom configuration.\n",
        encoding="utf-8",
    )

    # Auto-loading should happen during import in the package root directory
    payload = json.loads(_run_isolated_import(package_root))
    assert payload["short_arrays"] == "compact"
    assert payload["use_short_name"] is True
    assert payload["named_unit"] is False
    assert payload["include_params"] is True

    # Demonstrate the package structure was created correctly
    assert package_root.exists()
    assert (package_root / "pyproject.toml").exists()
    assert (package_root / "src" / "my_physics_project" / "__init__.py").exists()
    assert (package_root / "README.md").exists()
