"""Tests for --input-model option."""

from __future__ import annotations

import sys
import types
from argparse import Namespace
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from datamodel_code_generator import __main__ as main_module
from datamodel_code_generator import arguments
from datamodel_code_generator.__main__ import Exit
from tests.conftest import assert_output, freeze_time
from tests.main.conftest import run_main_with_args

if TYPE_CHECKING:
    from collections.abc import Iterator, Sequence

EXPECTED_INPUT_MODEL_PATH = Path(__file__).parent / "data" / "expected" / "main" / "input_model"
TIMESTAMP = "1985-10-26T01:21:00-07:00"
_MISSING_SYS_MODULE = object()


def _assert_sys_module_missing(module_name: str) -> None:
    """Assert sys.modules does not contain module_name."""
    __tracebackhide__ = True
    if module_name in sys.modules:  # pragma: no cover
        pytest.fail(f"Expected sys.modules to not contain {module_name!r}")


def _assert_sys_module_is(module_name: str, expected_module: types.ModuleType) -> None:
    """Assert sys.modules contains the expected module object."""
    __tracebackhide__ = True
    actual_module = sys.modules.get(module_name)
    if actual_module is not expected_module:  # pragma: no cover
        pytest.fail(f"Expected sys.modules[{module_name!r}] to be restored")


def _assert_sys_modules_with_prefix(module_prefix: str, expected_modules: set[str]) -> None:
    """Assert sys.modules keys with module_prefix match the expected set."""
    __tracebackhide__ = True
    actual_modules = {module_name for module_name in sys.modules if module_name.startswith(module_prefix)}
    if actual_modules != expected_modules:  # pragma: no cover
        pytest.fail(f"Expected sys.modules keys with prefix {module_prefix!r} to be restored")


@contextmanager
def _without_sys_module(module_name: str) -> Iterator[None]:
    """Temporarily remove a sys.modules entry and restore the previous state."""
    previous_module = sys.modules.pop(module_name, _MISSING_SYS_MODULE)
    try:
        yield
    finally:
        if previous_module is _MISSING_SYS_MODULE:
            sys.modules.pop(module_name, None)
        else:
            sys.modules[module_name] = previous_module


def _input_model_args(
    input_models: str | Sequence[str],
    *,
    output_path: Path | None = None,
    extra_args: Sequence[str] | None = None,
) -> list[str]:
    args: list[str] = []
    match input_models:
        case str():
            args.extend(["--input-model", input_models])
        case _:
            for input_model in input_models:
                args.extend(["--input-model", input_model])
    if output_path is not None:
        args.extend(["--output", str(output_path)])
    if not (extra := list(extra_args or ())):
        return args
    args.extend(extra)
    return args


def run_input_model_and_assert(
    *,
    input_model: str,
    output_path: Path,
    expected_file: Path,
    extra_args: Sequence[str] | None = None,
) -> None:
    """Run main with --input-model and assert results."""
    __tracebackhide__ = True
    with freeze_time(TIMESTAMP):
        run_main_with_args(
            _input_model_args(input_model, output_path=output_path, extra_args=extra_args),
            use_parsed_source_cache=False,
            use_builtin_default_formatter=False,
            isolate_model_template_cache=True,
        )
    assert_output(output_path.read_text(encoding="utf-8"), expected_file)


def run_input_model_error_and_assert(
    *,
    input_model: str,
    extra_args: Sequence[str] | None = None,
    capsys: pytest.CaptureFixture[str],
    expected_stderr_contains: str,
) -> None:
    """Run main with --input-model expecting error and assert stderr."""
    __tracebackhide__ = True
    run_main_with_args(
        _input_model_args(input_model, extra_args=extra_args),
        expected_exit=Exit.ERROR,
        capsys=capsys,
        expected_stderr_contains=expected_stderr_contains,
        use_parsed_source_cache=False,
        use_builtin_default_formatter=False,
        isolate_model_template_cache=True,
    )


@pytest.fixture(autouse=True)
def reset_namespace(monkeypatch: pytest.MonkeyPatch) -> None:
    """Reset the global namespace before each test."""
    new_namespace = Namespace(no_color=False)
    monkeypatch.setattr(arguments, "namespace", new_namespace)
    monkeypatch.setattr(main_module, "namespace", new_namespace)


@pytest.mark.cli_doc(
    options=["--input-model"],
    option_description="""Import a Python type or dict schema from a module or Python file.

Use the format `module:Object` or `path/to/file.py:Object` to specify the type.""",
    cli_args=["--input-model", "mymodule:MyModel"],
    input_model="mymodule:MyModel",
    expected_stdout="",
)
def test_input_model_pydantic_basemodel(tmp_path: Path) -> None:
    """Test Pydantic BaseModel input converts to Pydantic BaseModel output (default)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "pydantic_basemodel.py",
    )


def test_input_model_pydantic_to_typeddict(tmp_path: Path) -> None:
    """Test generating TypedDict from Pydantic model."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "pydantic_to_typeddict.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_pydantic_with_jsonschema_type(tmp_path: Path) -> None:
    """Test --input-model with explicit jsonschema input-file-type."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "pydantic_with_jsonschema.py",
        extra_args=["--input-file-type", "jsonschema"],
    )


def test_input_model_pydantic_non_jsonschema_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when Pydantic model used with non-jsonschema input-file-type."""
    run_input_model_error_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        extra_args=["--input-file-type", "openapi", "--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="--input-file-type must be 'jsonschema'",
    )


def test_input_model_dict_with_jsonschema(tmp_path: Path) -> None:
    """Test dict input with --input-file-type jsonschema."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.dict_schemas:USER_SCHEMA",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "dict_with_jsonschema.py",
        extra_args=["--input-file-type", "jsonschema"],
    )


def test_input_model_dict_without_type_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test that dict without --input-file-type raises error."""
    run_input_model_error_and_assert(
        input_model="tests.data.python.input_model.dict_schemas:USER_SCHEMA",
        extra_args=["--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="--input-file-type is required",
    )


def test_input_model_dict_openapi(tmp_path: Path) -> None:
    """Test dict input as OpenAPI spec."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.dict_schemas:OPENAPI_SPEC",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "dict_openapi.py",
        extra_args=["--input-file-type", "openapi"],
    )


def test_input_model_std_dataclass(tmp_path: Path) -> None:
    """Test stdlib dataclass input converts to Pydantic BaseModel output (default)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.dataclass_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "std_dataclass.py",
    )


def test_input_model_pydantic_dataclass(tmp_path: Path) -> None:
    """Test Pydantic dataclass input converts to Pydantic BaseModel output (default)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_dataclass_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "pydantic_dataclass.py",
    )


def test_input_model_typeddict(tmp_path: Path) -> None:
    """Test TypedDict input converts to Pydantic BaseModel output (default)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.typeddict_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "typeddict.py",
    )


def test_input_model_dataclass_non_jsonschema_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when dataclass used with non-jsonschema input-file-type."""
    run_input_model_error_and_assert(
        input_model="tests.data.python.input_model.dataclass_models:User",
        extra_args=["--input-file-type", "openapi", "--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="--input-file-type must be 'jsonschema'",
    )


def test_input_model_invalid_format(capsys: pytest.CaptureFixture[str]) -> None:
    """Test error when colon is missing."""
    run_input_model_error_and_assert(
        input_model="pydantic.BaseModel",
        capsys=capsys,
        expected_stderr_contains="Invalid --input-model format",
    )


def test_input_model_invalid_module(capsys: pytest.CaptureFixture[str]) -> None:
    """Test error when module doesn't exist."""
    run_input_model_error_and_assert(
        input_model="nonexistent_module_12345:Model",
        capsys=capsys,
        expected_stderr_contains="Cannot find module",
    )


def test_input_model_invalid_attr(capsys: pytest.CaptureFixture[str]) -> None:
    """Test error when class doesn't exist in module."""
    run_input_model_error_and_assert(
        input_model="pydantic:NonexistentClass12345",
        capsys=capsys,
        expected_stderr_contains="has no attribute",
    )


def test_input_model_unsupported_type(capsys: pytest.CaptureFixture[str]) -> None:
    """Test error when object is not a supported type."""
    run_input_model_error_and_assert(
        input_model="pathlib:Path",
        capsys=capsys,
        expected_stderr_contains="is not a supported type",
    )


def test_input_model_mutual_exclusion_with_input(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test --input-model cannot be used with --input."""
    run_input_model_error_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        extra_args=["--input", str(tmp_path / "schema.json")],
        capsys=capsys,
        expected_stderr_contains="--input-model cannot be used with --input or --url",
    )


def test_input_model_mutual_exclusion_with_url(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test --input-model cannot be used with --url."""
    run_input_model_error_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        extra_args=["--url", "https://example.com/schema.json"],
        capsys=capsys,
        expected_stderr_contains="--input-model cannot be used with --input or --url",
    )


def test_input_model_mutual_exclusion_with_watch(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test --input-model cannot be used with --watch."""
    run_input_model_error_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        extra_args=["--watch", "--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="--watch cannot be used with --input-model",
    )


def test_input_model_adds_cwd_to_sys_path(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test that --input-model adds cwd to sys.path if not present."""
    cwd = str(tmp_path)
    monkeypatch.chdir(tmp_path)
    assert cwd not in sys.path

    original_sys_path = sys.path.copy()
    try:
        run_input_model_and_assert(
            input_model="tests.data.python.input_model.pydantic_models:User",
            output_path=tmp_path / "output.py",
            expected_file=EXPECTED_INPUT_MODEL_PATH / "pydantic_basemodel.py",
        )
        assert cwd in sys.path
    finally:
        sys.path[:] = original_sys_path


def test_input_model_path_format(tmp_path: Path) -> None:
    """Test --input-model with path format (path/to/file.py:Object)."""
    run_input_model_and_assert(
        input_model="tests/data/python/input_model/pydantic_models.py:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "path_format.py",
    )


def test_input_model_path_format_restores_sys_modules(tmp_path: Path) -> None:
    """Test path-based --input-model does not keep temporary modules alive."""
    model_path = tmp_path / "temporary_input_model.py"
    model_path.write_text(
        "from pydantic import BaseModel\n\nclass User(BaseModel):\n    name: str\n    age: int\n",
        encoding="utf-8",
    )
    module_name = model_path.stem

    with _without_sys_module(module_name):
        run_input_model_and_assert(
            input_model=f"{model_path}:User",
            output_path=tmp_path / "output.py",
            expected_file=EXPECTED_INPUT_MODEL_PATH / "path_format.py",
        )

        _assert_sys_module_missing(module_name)


def test_input_model_path_format_restores_existing_sys_modules(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test path-based --input-model restores a pre-existing sys.modules entry."""
    model_path = tmp_path / "existing_input_model.py"
    model_path.write_text(
        "from pydantic import BaseModel\n\nclass User(BaseModel):\n    name: str\n    age: int\n",
        encoding="utf-8",
    )
    module_name = model_path.stem
    existing_module = types.ModuleType(module_name)
    monkeypatch.setitem(sys.modules, module_name, existing_module)

    run_input_model_and_assert(
        input_model=f"{model_path}:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "path_format.py",
    )

    _assert_sys_module_is(module_name, existing_module)


def test_without_sys_module_restores_existing_module() -> None:
    """Test _without_sys_module restores the previous sys.modules entry."""
    module_name = "temporary_existing_input_model"
    existing_module = types.ModuleType(module_name)

    with _without_sys_module(module_name):
        sys.modules[module_name] = existing_module
        with _without_sys_module(module_name):
            _assert_sys_module_missing(module_name)

        _assert_sys_module_is(module_name, existing_module)

    _assert_sys_module_missing(module_name)


def test_input_model_path_format_filename_only(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test --input-model with filename.py format (no slash)."""
    from pathlib import Path as PathLib

    monkeypatch.chdir(PathLib("tests/data/python/input_model"))
    run_input_model_and_assert(
        input_model="pydantic_models.py:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "path_format_filename_only.py",
    )


def test_input_model_path_file_not_found(capsys: pytest.CaptureFixture[str]) -> None:
    """Test error when file path doesn't exist."""
    run_input_model_error_and_assert(
        input_model="./nonexistent_file_12345.py:Model",
        capsys=capsys,
        expected_stderr_contains="File not found",
    )


def test_input_model_path_cannot_load(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test error when spec_from_file_location returns None."""
    import importlib.util

    test_file = tmp_path / "test_model.py"
    test_file.write_text("class Model: pass")

    monkeypatch.setattr(importlib.util, "spec_from_file_location", lambda *_a, **_kw: None)

    run_input_model_error_and_assert(
        input_model=f"{test_file}:Model",
        capsys=capsys,
        expected_stderr_contains="Cannot load module",
    )


def test_input_model_module_import_error(
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test error when module import fails."""
    import importlib
    import importlib.util

    class FakeSpec:
        name = "fake_module"

    def fake_find_spec(_name: str) -> FakeSpec:
        return FakeSpec()

    def fake_import_module(_name: str) -> None:
        msg = "fake import error"
        raise ImportError(msg)

    monkeypatch.setattr(importlib.util, "find_spec", fake_find_spec)
    monkeypatch.setattr(importlib, "import_module", fake_import_module)

    run_input_model_error_and_assert(
        input_model="some_module:Model",
        capsys=capsys,
        expected_stderr_contains="Cannot import module",
    )


def test_path_module_name_keeps_same_file_module(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test path module naming keeps the stem when the loaded module is the same file."""
    from datamodel_code_generator.input_model import _get_path_module_name

    model_path = (tmp_path / "same_file_model.py").resolve()
    model_path.write_text("class Model: pass\n", encoding="utf-8")
    module_name = model_path.stem
    existing_module = types.ModuleType(module_name)
    existing_module.__file__ = str(model_path)
    monkeypatch.setitem(sys.modules, module_name, existing_module)

    assert _get_path_module_name(model_path) == module_name


def test_path_module_name_falls_back_when_existing_file_cannot_resolve(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test path module naming keeps the stem when an existing module file cannot resolve."""
    from datamodel_code_generator.input_model import _get_path_module_name

    model_path = (tmp_path / "unresolvable_existing_model.py").resolve()
    model_path.write_text("class Model: pass\n", encoding="utf-8")
    module_name = model_path.stem
    existing_module = types.ModuleType(module_name)
    existing_module.__file__ = "__unresolvable_existing_model__"
    monkeypatch.setitem(sys.modules, module_name, existing_module)

    def fake_resolve(_path: Path, *_args: object, **_kwargs: object) -> Path:
        msg = "cannot resolve"
        raise OSError(msg)

    monkeypatch.setattr(Path, "resolve", fake_resolve)

    assert _get_path_module_name(model_path) == module_name


def test_load_module_from_path_restores_sys_modules_on_exec_error(tmp_path: Path) -> None:
    """Test path module loading restores sys.modules when module execution fails."""
    from datamodel_code_generator.input_model import _load_module_from_path

    model_path = (tmp_path / "failing_input_model.py").resolve()
    model_path.write_text("raise RuntimeError('module failed')\n", encoding="utf-8")
    module_name = model_path.stem

    with _without_sys_module(module_name):
        with pytest.raises(RuntimeError, match="module failed"):
            _load_module_from_path(model_path, str(model_path))

        _assert_sys_module_missing(module_name)


def test_is_input_model_base_schema_requires_dict() -> None:
    """Test base schema detection returns false for non-dict values."""
    from datamodel_code_generator.input_model import _is_input_model_base_schema

    assert not _is_input_model_base_schema("not a schema")


# ============================================================================
# x-python-type preservation tests
# ============================================================================


@pytest.mark.parametrize(
    "test_id",
    [
        "set_type",
        "frozenset_type",
        "mapping_type",
        "sequence_type",
        "nested_model_types",
    ],
)
def test_input_model_preserves_python_types(tmp_path: Path, test_id: str) -> None:
    """Test that Python collection types are preserved when converting Pydantic model."""
    del test_id
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "model_with_python_types.py",
    )


@pytest.mark.parametrize(
    ("output_model_type", "expected_file"),
    [
        ("typing.TypedDict", "model_with_python_types_typeddict.py"),
        ("dataclasses.dataclass", "model_with_python_types_dataclass.py"),
    ],
)
def test_input_model_x_python_type_output_formats(tmp_path: Path, output_model_type: str, expected_file: str) -> None:
    """Test that x-python-type works with different output model types."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / expected_file,
        extra_args=["--output-model-type", output_model_type],
    )


def test_input_model_dataclass_with_python_types(tmp_path: Path) -> None:
    """Test that Set/Mapping types are preserved from dataclass input."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.dataclass_models:DataclassWithPythonTypes",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "dataclass_with_python_types.py",
    )


def test_input_model_recursive_model_types(tmp_path: Path) -> None:
    """Test that recursive models handle x-python-type correctly."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:RecursiveNode",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "recursive_model_types.py",
    )


@pytest.mark.parametrize(
    "test_id",
    [
        "optional_set",
        "union_none_frozenset",
        "optional_mapping_union_syntax",
    ],
)
def test_input_model_optional_types(tmp_path: Path, test_id: str) -> None:
    """Test that optional/union Python types are preserved when converting Pydantic model."""
    del test_id
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "model_with_python_types.py",
    )


# ============================================================================
# Callable and unserializable type tests
# ============================================================================


@pytest.mark.parametrize(
    "test_id",
    [
        "basic",
        "multi_param",
        "variadic",
        "no_param",
        "optional",
        "type_field",
        "nested",
    ],
)
def test_input_model_callable_types(tmp_path: Path, test_id: str) -> None:
    """Test that Callable and Type annotations are preserved when converting Pydantic model."""
    del test_id
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithCallableTypes",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "model_with_callable_types.py",
    )


def test_input_model_nested_model_with_callable(tmp_path: Path) -> None:
    """Test that nested models with Callable types in $defs are processed."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithNestedCallable",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "nested_model_with_callable.py",
    )


def test_input_model_custom_class(tmp_path: Path) -> None:
    """Test that custom classes trigger handle_invalid_for_json_schema."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithCustomClass",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "custom_class.py",
    )


def test_input_model_union_callable(tmp_path: Path) -> None:
    """Test that Union[Callable, int] and raw Callable are preserved."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithUnionCallable",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "union_callable.py",
    )


def test_input_model_custom_generic_type_import(tmp_path: Path) -> None:
    """Test that custom generic types are properly imported with full module path."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithCustomGeneric",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "custom_generic_type_import.py",
    )


def test_input_model_default_put_dict_import(tmp_path: Path) -> None:
    """Test that DefaultPutDict generic type is properly imported from parser module."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:ModelWithDefaultPutDict",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "default_put_dict_import.py",
    )


# ============================================================================
# --input-model-ref-strategy tests
# ============================================================================


@pytest.mark.cli_doc(
    options=["--input-model-ref-strategy"],
    option_description="""Strategy for referenced types when using --input-model.

The `--input-model-ref-strategy` option determines whether to regenerate or import
referenced types. Use `regenerate-all` (default) to regenerate all types,
`reuse-foreign` to import types from different families (like enums when generating
dataclasses) while regenerating same-family types, or `reuse-all` to import all
referenced types directly.""",
    cli_args=["--input-model-ref-strategy", "reuse-foreign"],
    input_model="mymodule:MyModel",
    expected_stdout="",
)
def test_input_model_ref_strategy_regenerate_all_default(tmp_path: Path) -> None:
    """Test default regenerate-all strategy regenerates all types."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.nested_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_regenerate_all.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_ref_strategy_regenerate_all_explicit(tmp_path: Path) -> None:
    """Test explicit regenerate-all strategy regenerates all types."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.nested_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_regenerate_all.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "regenerate-all",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign(tmp_path: Path) -> None:
    """Test reuse-foreign imports enum (always) and same-family types."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.nested_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_no_regeneration(tmp_path: Path) -> None:
    """Test reuse-foreign imports only types compatible with output (enum always, same family)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.nested_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_all(tmp_path: Path) -> None:
    """Test reuse-all strategy imports all referenced types."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.nested_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_all.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-all",
        ],
    )


def test_input_model_path_ref_strategy_reuse_all_keeps_stem_imports(tmp_path: Path) -> None:
    """Test path-based reuse-all keeps the existing stem-based import path."""
    run_input_model_and_assert(
        input_model="tests/data/python/input_model/nested_models.py:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "path_ref_strategy_reuse_all.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-all",
        ],
    )


def test_input_model_ref_strategy_reuse_all_no_regeneration(tmp_path: Path) -> None:
    """Test reuse-all strategy does not regenerate any referenced classes."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.nested_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_all.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-all",
        ],
    )


def test_input_model_ref_strategy_without_input_model(tmp_path: Path) -> None:
    """Test that --input-model-ref-strategy is ignored without --input-model."""
    schema_file = tmp_path / "schema.json"
    schema_file.write_text('{"type": "object", "properties": {"name": {"type": "string"}}}')

    args = [
        "--input",
        str(schema_file),
        "--output",
        str(tmp_path / "output.py"),
        "--input-model-ref-strategy",
        "reuse-all",
    ]
    run_main_with_args(
        args,
        use_parsed_source_cache=False,
        use_builtin_default_formatter=False,
        isolate_model_template_cache=True,
    )


def test_input_model_ref_strategy_no_nested_types(tmp_path: Path) -> None:
    """Test reuse-all strategy with Pydantic model that has no nested types (no $defs)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.pydantic_models:User",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_no_nested_types.py",
        extra_args=[
            "--output-model-type",
            "dataclasses.dataclass",
            "--input-model-ref-strategy",
            "reuse-all",
        ],
    )


def test_input_model_ref_strategy_dataclass_reuse_foreign(tmp_path: Path) -> None:
    """Test reuse-foreign strategy with dataclass input."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.dataclass_nested:Task",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_dataclass_reuse_foreign.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_typeddict_reuse_all(tmp_path: Path) -> None:
    """Test reuse-all strategy with TypedDict input."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.typeddict_nested:Member",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_typeddict_reuse_all.py",
        extra_args=[
            "--output-model-type",
            "dataclasses.dataclass",
            "--input-model-ref-strategy",
            "reuse-all",
        ],
    )


def test_input_model_ref_strategy_typeddict_reuse_foreign(tmp_path: Path) -> None:
    """Test reuse-foreign strategy with TypedDict input imports enum, regenerates typeddict."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.typeddict_nested:Member",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_typeddict_reuse_foreign.py",
        extra_args=[
            "--output-model-type",
            "dataclasses.dataclass",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_same_family_typeddict(tmp_path: Path) -> None:
    """Test reuse-foreign imports TypedDict when output is TypedDict (same family)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.mixed_nested:ModelWithTypedDict",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign_same_family_typeddict.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_different_family_regenerate(tmp_path: Path) -> None:
    """Test reuse-foreign regenerates Pydantic model when output is TypedDict."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.mixed_nested:ModelWithPydantic",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign_different_family.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_same_family_dataclass(tmp_path: Path) -> None:
    """Test reuse-foreign imports dataclass when output is dataclass (same family)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.mixed_nested:ModelWithDataclass",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign_same_family_dataclass.py",
        extra_args=[
            "--output-model-type",
            "dataclasses.dataclass",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_mixed_types(tmp_path: Path) -> None:
    """Test reuse-foreign with mixed nested types (TypedDict, Pydantic, dataclass)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.mixed_nested:ModelWithMixed",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign_mixed_types.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_pydantic_output(tmp_path: Path) -> None:
    """Test reuse-foreign imports Pydantic when output is Pydantic (same family)."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.mixed_nested:ModelWithPydantic",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign_pydantic_output.py",
        extra_args=[
            "--output-model-type",
            "pydantic_v2.BaseModel",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_ref_strategy_reuse_foreign_msgspec_output(tmp_path: Path) -> None:
    """Test reuse-foreign regenerates non-msgspec types when output is msgspec."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.mixed_nested:ModelWithPydantic",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "ref_strategy_reuse_foreign_msgspec_output.py",
        extra_args=[
            "--output-model-type",
            "msgspec.Struct",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_config_class(tmp_path: Path) -> None:
    """Test that config classes like GenerateConfig are properly handled."""
    run_input_model_and_assert(
        input_model="datamodel_code_generator.config:GenerateConfig",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "config_class.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


# ============================================================================
# Inheritance support tests (single and multiple --input-model)
# ============================================================================


def run_multiple_input_models_and_assert(
    *,
    input_models: Sequence[str],
    output_path: Path,
    expected_file: Path,
    extra_args: Sequence[str] | None = None,
) -> None:
    """Run main with multiple --input-model and assert results."""
    __tracebackhide__ = True
    with freeze_time(TIMESTAMP):
        run_main_with_args(
            _input_model_args(input_models, output_path=output_path, extra_args=extra_args),
            use_parsed_source_cache=False,
            use_builtin_default_formatter=False,
            isolate_model_template_cache=True,
        )
    assert_output(output_path.read_text(encoding="utf-8"), expected_file)


def run_multiple_input_models_error_and_assert(
    *,
    input_models: Sequence[str],
    extra_args: Sequence[str] | None = None,
    capsys: pytest.CaptureFixture[str],
    expected_stderr_contains: str,
) -> None:
    """Run main with multiple --input-model expecting error and assert stderr."""
    __tracebackhide__ = True
    run_main_with_args(
        _input_model_args(input_models, extra_args=extra_args),
        expected_exit=Exit.ERROR,
        capsys=capsys,
        expected_stderr_contains=expected_stderr_contains,
        use_parsed_source_cache=False,
        use_builtin_default_formatter=False,
        isolate_model_template_cache=True,
    )


def test_input_model_single_with_inheritance(tmp_path: Path) -> None:
    """Test single --input-model with inherited model generates inheritance chain."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.inheritance_models:ChildA",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "single_inheritance.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_single_multi_level_inheritance(tmp_path: Path) -> None:
    """Test single --input-model with multi-level inheritance."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.inheritance_models:GrandChild",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "multi_level_inheritance.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_single_no_inheritance(tmp_path: Path) -> None:
    """Test single --input-model with model that has no inheritance."""
    run_input_model_and_assert(
        input_model="tests.data.python.input_model.inheritance_models:NoInheritance",
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "no_inheritance.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_multiple_forked_inheritance(tmp_path: Path) -> None:
    """Test multiple --input-model with forked inheritance shares common parent."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "forked_inheritance.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_multiple_mixed_inheritance(tmp_path: Path) -> None:
    """Test multiple --input-model with different inheritance depths."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:GrandChild",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "mixed_inheritance.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_multiple_with_pydantic_output(tmp_path: Path) -> None:
    """Test multiple --input-model works with Pydantic output."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "multiple_with_pydantic_output.py",
        extra_args=["--output-model-type", "pydantic_v2.BaseModel"],
    )


def test_input_model_multiple_with_dataclass_output(tmp_path: Path) -> None:
    """Test multiple --input-model works with dataclass output."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "multiple_with_dataclass_output.py",
        extra_args=["--output-model-type", "dataclasses.dataclass"],
    )


def test_input_model_multiple_non_basemodel_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when multiple --input-model includes non-BaseModel."""
    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.dict_schemas:USER_SCHEMA",
        ],
        extra_args=["--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="Multiple --input-model only supports Pydantic v2 BaseModel",
    )


def test_input_model_multiple_invalid_format_error(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when multiple --input-model has invalid format."""
    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "invalid_format_no_colon",
        ],
        capsys=capsys,
        expected_stderr_contains="Invalid --input-model format",
    )


def test_input_model_multiple_file_not_found_error(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when multiple --input-model file doesn't exist."""
    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "./nonexistent_file.py:Model",
        ],
        capsys=capsys,
        expected_stderr_contains="File not found",
    )


def test_input_model_multiple_module_not_found_error(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when multiple --input-model module doesn't exist."""
    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "nonexistent_module_xyz:Model",
        ],
        capsys=capsys,
        expected_stderr_contains="Cannot find module",
    )


def test_input_model_multiple_attribute_not_found_error(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when multiple --input-model attribute doesn't exist."""
    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:NonexistentModel",
        ],
        capsys=capsys,
        expected_stderr_contains="has no attribute",
    )


def test_input_model_multiple_non_jsonschema_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Test error when multiple --input-model used with non-jsonschema type."""
    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
        ],
        extra_args=["--input-file-type", "openapi", "--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="--input-file-type must be 'jsonschema'",
    )


def test_input_model_multiple_same_module(tmp_path: Path) -> None:
    """Test multiple --input-model from same module reuses module load."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
            "tests.data.python.input_model.inheritance_models:GrandChild",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "multiple_same_module.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_multiple_file_path_format(tmp_path: Path) -> None:
    """Test multiple --input-model with file path format."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests/data/python/input_model/inheritance_models.py:ChildA",
            "tests/data/python/input_model/inheritance_models.py:ChildB",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "forked_inheritance.py",
        extra_args=["--output-model-type", "typing.TypedDict"],
    )


def test_input_model_multiple_file_path_same_basename_forward_refs(tmp_path: Path) -> None:
    """Test same-basename path modules keep separate namespaces for forward refs."""
    first_dir = tmp_path / "first"
    second_dir = tmp_path / "second"
    first_dir.mkdir()
    second_dir.mkdir()
    first_model_path = first_dir / "models.py"
    second_model_path = second_dir / "models.py"
    first_model_path.write_text(
        "from __future__ import annotations\n\n"
        "from pydantic import BaseModel\n\n"
        "class UserA(BaseModel):\n"
        "    friend: FriendA\n\n"
        "class FriendA(BaseModel):\n"
        "    alpha: str\n",
        encoding="utf-8",
    )
    second_model_path.write_text(
        "from __future__ import annotations\n\n"
        "from pydantic import BaseModel\n\n"
        "class UserB(BaseModel):\n"
        "    friend: FriendB\n\n"
        "class FriendB(BaseModel):\n"
        "    beta: int\n",
        encoding="utf-8",
    )
    module_name = first_model_path.stem
    temporary_module_prefix = "_datamodel_code_generator_input_model_"
    existing_temporary_modules = {
        module_name for module_name in sys.modules if module_name.startswith(temporary_module_prefix)
    }

    with _without_sys_module(module_name):
        run_multiple_input_models_and_assert(
            input_models=[
                f"{first_model_path}:UserA",
                f"{second_model_path}:UserB",
            ],
            output_path=tmp_path / "output.py",
            expected_file=EXPECTED_INPUT_MODEL_PATH / "multiple_same_basename_paths.py",
        )

        _assert_sys_module_missing(module_name)
        _assert_sys_modules_with_prefix(temporary_module_prefix, existing_temporary_modules)


def test_input_model_multiple_file_path_format_restores_sys_modules(tmp_path: Path) -> None:
    """Test multiple path-based --input-model entries do not keep temporary modules alive."""
    model_path = tmp_path / "multiple_input_model.py"
    model_path.write_text(
        "from pydantic import BaseModel\n\n"
        "class GrandParent(BaseModel):\n"
        "    grand_field: str\n\n"
        "class Parent(GrandParent):\n"
        "    parent_field: int\n\n"
        "class ChildA(Parent):\n"
        "    child_a_field: float\n\n"
        "class ChildB(Parent):\n"
        "    child_b_field: bool\n",
        encoding="utf-8",
    )
    module_name = model_path.stem

    with _without_sys_module(module_name):
        run_multiple_input_models_and_assert(
            input_models=[
                f"{model_path}:ChildA",
                f"{model_path}:ChildB",
            ],
            output_path=tmp_path / "output.py",
            expected_file=EXPECTED_INPUT_MODEL_PATH / "forked_inheritance.py",
            extra_args=["--output-model-type", "typing.TypedDict"],
        )

        _assert_sys_module_missing(module_name)


def test_input_model_multiple_with_ref_strategy(tmp_path: Path) -> None:
    """Test multiple --input-model works with --input-model-ref-strategy."""
    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "forked_inheritance.py",
        extra_args=[
            "--output-model-type",
            "typing.TypedDict",
            "--input-model-ref-strategy",
            "reuse-foreign",
        ],
    )


def test_input_model_multiple_cannot_load_module_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test error when spec_from_file_location returns None for multiple models."""
    import importlib.util

    test_file = tmp_path / "test_model.py"
    test_file.write_text("from pydantic import BaseModel\nclass Model(BaseModel): pass")

    original_spec_from_file_location = importlib.util.spec_from_file_location

    def mock_spec(*args: object, **kwargs: object) -> None:
        if hasattr(mock_spec, "called"):
            return None
        mock_spec.called = True  # type: ignore[attr-defined]
        return original_spec_from_file_location(*args, **kwargs)

    monkeypatch.setattr(importlib.util, "spec_from_file_location", mock_spec)

    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests/data/python/input_model/inheritance_models.py:ChildA",
            f"{test_file}:Model",
        ],
        extra_args=["--output", str(tmp_path / "output.py")],
        capsys=capsys,
        expected_stderr_contains="Cannot load module",
    )


def test_input_model_multiple_import_error(
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test error when module import fails for multiple models."""
    import importlib
    import importlib.util

    class FakeSpec:
        name = "fake_module"

    original_find_spec = importlib.util.find_spec
    original_import_module = importlib.import_module
    call_count = 0

    def fake_find_spec(name: str, *args: object, **kwargs: object) -> FakeSpec | None:
        nonlocal call_count
        call_count += 1
        if "nonexistent_import_module" in name:
            return FakeSpec()
        return original_find_spec(name, *args, **kwargs)

    def fake_import_module(name: str, *args: object, **kwargs: object) -> object:
        if "nonexistent_import_module" in name:
            msg = "fake import error"
            raise ImportError(msg)
        return original_import_module(name, *args, **kwargs)

    monkeypatch.setattr(importlib.util, "find_spec", fake_find_spec)
    monkeypatch.setattr(importlib, "import_module", fake_import_module)

    run_multiple_input_models_error_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "nonexistent_import_module:Model",
        ],
        capsys=capsys,
        expected_stderr_contains="Cannot import module",
    )


def test_input_model_empty_child_no_properties(
    tmp_path: Path,
) -> None:
    """Test inheritance with empty child that adds no properties."""
    run_multiple_input_models_and_assert(
        input_models=["tests.data.python.input_model.inheritance_models:EmptyChild"],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "empty_child_no_properties.py",
    )


def test_input_model_optional_only_child_no_required(
    tmp_path: Path,
) -> None:
    """Test inheritance with child that adds only optional fields."""
    run_multiple_input_models_and_assert(
        input_models=["tests.data.python.input_model.inheritance_models:OptionalOnlyChild"],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "optional_only_child_no_required.py",
    )


def test_input_model_cwd_already_in_path(
    tmp_path: Path,
) -> None:
    """Test that cwd is not duplicated in sys.path when already present."""
    from pathlib import Path as _Path

    cwd = str(_Path.cwd())
    initial_count = sys.path.count(cwd)

    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "tests.data.python.input_model.inheritance_models:ChildB",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "multiple_with_pydantic_output.py",
        extra_args=["--output-model-type", "pydantic_v2.BaseModel"],
    )
    final_count = sys.path.count(cwd)
    assert final_count <= initial_count + 1


def test_input_model_multiple_py_file_without_path_separator(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Test loading .py file without path separator (just filename.py)."""
    model_content = """
from pydantic import BaseModel

class TempModel(BaseModel):
    value: str
"""
    temp_file = tmp_path / "temp_model.py"
    temp_file.write_text(model_content)

    monkeypatch.chdir(tmp_path)

    run_multiple_input_models_and_assert(
        input_models=[
            "tests.data.python.input_model.inheritance_models:ChildA",
            "temp_model.py:TempModel",
        ],
        output_path=tmp_path / "output.py",
        expected_file=EXPECTED_INPUT_MODEL_PATH / "multiple_py_file_without_path_separator.py",
    )


def test_input_model_config_string_coercion(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that string input_model in config is coerced to list."""
    config_content = """\
[tool.datamodel-codegen]
input-model = "tests.data.python.input_model.inheritance_models:NoInheritance"
output-model-type = "typing.TypedDict"
"""
    config_file = tmp_path / "pyproject.toml"
    config_file.write_text(config_content)
    monkeypatch.chdir(tmp_path)

    output_path = tmp_path / "output.py"
    with freeze_time(TIMESTAMP):
        run_main_with_args(
            ["--output", str(output_path)],
            use_parsed_source_cache=False,
            use_builtin_default_formatter=False,
            isolate_model_template_cache=True,
        )
    assert_output(
        output_path.read_text(encoding="utf-8"),
        EXPECTED_INPUT_MODEL_PATH / "no_inheritance.py",
    )


def test_input_model_output_model_type_default() -> None:
    """Test that output_model_type defaults to PydanticV2BaseModel when not specified."""
    from datamodel_code_generator import InputFileType
    from datamodel_code_generator.input_model import load_model_schema

    schema = load_model_schema(
        ["tests.data.python.input_model.inheritance_models:NoInheritance"],
        InputFileType.JsonSchema,
        None,
        None,
    )
    assert schema.get("title") == "NoInheritance"
    assert "properties" in schema


# ============================================================================
# Unit tests for helper functions (coverage)
# ============================================================================


def test_simple_type_name_none_type() -> None:
    """Test _simple_type_name with NoneType."""
    from datamodel_code_generator.input_model import _simple_type_name

    result = _simple_type_name(type(None))
    assert result == "None"


def test_simple_type_name_generic_type() -> None:
    """Test _simple_type_name with generic type (has origin)."""
    from datamodel_code_generator.input_model import _simple_type_name

    result = _simple_type_name(list[str])
    assert result == "list[str]"


def test_full_type_name_string_annotation() -> None:
    """Test _full_type_name with string annotation."""
    from datamodel_code_generator.input_model import _full_type_name

    result = _full_type_name("SomeType")  # pyright: ignore[reportArgumentType]
    assert result == "SomeType"


def test_full_type_name_forward_ref() -> None:
    """Test _full_type_name with ForwardRef."""
    from typing import ForwardRef

    from datamodel_code_generator.input_model import _full_type_name

    ref = ForwardRef("MyClass")
    result = _full_type_name(ref)  # pyright: ignore[reportArgumentType]
    assert result == "MyClass"


def test_full_type_name_generic_no_args() -> None:
    """Test _full_type_name with generic type that has no args (covers line 365)."""
    from datamodel_code_generator.input_model import _full_type_name

    # Create a GenericAlias with origin=list but args=() - hits line 365
    # list.__class_getitem__(()) creates list[()] which has origin but no args
    generic_with_no_args = list.__class_getitem__(())
    result = _full_type_name(generic_with_no_args)  # pyright: ignore[reportArgumentType]
    assert result == "list"


def test_full_type_name_typing_special() -> None:
    """Test _full_type_name with typing module special forms."""
    from typing import Any

    from datamodel_code_generator.input_model import _full_type_name

    result = _full_type_name(Any)  # pyright: ignore[reportArgumentType]
    assert result == "Any"


def test_serialize_python_type_full_annotated() -> None:
    """Test _serialize_python_type_full with Annotated type."""
    from typing import Annotated

    from datamodel_code_generator.input_model import _serialize_python_type_full

    # Annotated with a custom type
    result = _serialize_python_type_full(Annotated[int, "some_metadata"])
    assert result == "int"


def test_full_type_name_builtin_type() -> None:
    """Test _full_type_name with builtin type (module='builtins')."""
    from datamodel_code_generator.input_model import _full_type_name

    # int is a builtin type with module='builtins'
    result = _full_type_name(int)
    assert result == "int"


def test_full_type_name_collections_abc_type() -> None:
    """Test _full_type_name with collections.abc type."""
    from collections.abc import Iterable

    from datamodel_code_generator.input_model import _full_type_name

    # Iterable is from collections.abc
    result = _full_type_name(Iterable)  # pyright: ignore[reportArgumentType]
    assert result == "Iterable"
