"""Tests for inputs module."""

import json
from pathlib import Path

import pytest
from pydantic import ValidationError

from dotpromptz.inputs import (
    _load_file,
    _validate_file_path,
    infer_schema,
    infer_schema_from_example,
    resolve_files,
    resolve_input,
)

# Test data constants
SAMPLE_DICT = {'name': 'Alice', 'age': 30}
SAMPLE_LIST = [{'id': 1}, {'id': 2}]


class TestResolveInputNoneAndDict:
    """Test resolve_input with None and dict inputs."""

    def test_none_input_returns_empty_single_mode(self, tmp_path: Path) -> None:
        """None input returns empty list."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input(None, prompt_file)
        assert records == []

    def test_dict_input_returns_single_mode(self, tmp_path: Path) -> None:
        """Dict input returns [dict]."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input(SAMPLE_DICT, prompt_file)
        assert records == [SAMPLE_DICT]


class TestResolveInputList:
    """Test resolve_input with list inputs."""

    def test_empty_list_returns_batch_mode(self, tmp_path: Path) -> None:
        """Empty list returns []."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input([], prompt_file)
        assert records == []

    def test_list_of_dicts_returns_batch_mode(self, tmp_path: Path) -> None:
        """List[dict] returns same list."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input(SAMPLE_LIST, prompt_file)
        assert records == SAMPLE_LIST

    def test_list_mixed_dict_and_int_raises(self, tmp_path: Path) -> None:
        """List with mixed types (dict and int) raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='All items in data list must be dicts'):
            resolve_input([{'a': 1}, 123], prompt_file)

    def test_list_of_invalid_type_raises(self, tmp_path: Path) -> None:
        """List with invalid type (int) raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='data list must contain dicts, not int'):
            resolve_input([123, 456], prompt_file)


class TestResolveInputInvalidTypes:
    """Test resolve_input rejects invalid input types."""

    def test_str_input_raises(self, tmp_path: Path) -> None:
        """String input raises ValueError (use files instead)."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='Unsupported input type: str'):
            resolve_input('input.json', prompt_file)


class TestValidateFilePath:
    """Test _validate_file_path validation."""

    def test_relative_path_resolves_correctly(self, tmp_path: Path) -> None:
        """Normal relative path resolves within prompt_dir."""
        prompt_dir = tmp_path
        test_file = tmp_path / 'input.json'
        test_file.touch()

        resolved = _validate_file_path('input.json', prompt_dir)
        assert resolved == test_file.resolve()

    def test_nested_relative_path_resolves(self, tmp_path: Path) -> None:
        """Nested relative path (subdir/file) resolves correctly."""
        prompt_dir = tmp_path
        subdir = tmp_path / 'data'
        subdir.mkdir()
        test_file = subdir / 'input.json'
        test_file.touch()

        resolved = _validate_file_path('data/input.json', prompt_dir)
        assert resolved == test_file.resolve()

    def test_absolute_path_resolves(self, tmp_path: Path) -> None:
        """Absolute path resolves correctly."""
        prompt_dir = tmp_path / 'prompts'
        prompt_dir.mkdir()
        outside_file = tmp_path / 'outside.json'
        outside_file.touch()

        resolved = _validate_file_path(str(outside_file.resolve()), prompt_dir)
        assert resolved == outside_file.resolve()


class TestLoadFile:
    """Test _load_file for various formats."""

    def test_load_json_dict(self, tmp_path: Path) -> None:
        """Load valid JSON dict."""
        file_path = tmp_path / 'test.json'
        file_path.write_text(json.dumps(SAMPLE_DICT), encoding='utf-8')
        data = _load_file(file_path)
        assert data == SAMPLE_DICT

    def test_load_json_list(self, tmp_path: Path) -> None:
        """Load valid JSON list of dicts."""
        file_path = tmp_path / 'test.json'
        file_path.write_text(json.dumps(SAMPLE_LIST), encoding='utf-8')
        data = _load_file(file_path)
        assert data == SAMPLE_LIST

    def test_load_json_invalid_syntax_raises(self, tmp_path: Path) -> None:
        """Invalid JSON syntax raises ValueError."""
        file_path = tmp_path / 'bad.json'
        file_path.write_text('{invalid json', encoding='utf-8')
        with pytest.raises(ValueError, match='Invalid JSON'):
            _load_file(file_path)

    def test_load_json_array_with_non_dict_raises(self, tmp_path: Path) -> None:
        """JSON array with non-dict items raises ValueError."""
        file_path = tmp_path / 'bad.json'
        file_path.write_text('[1, 2, 3]', encoding='utf-8')
        with pytest.raises(ValueError, match='must contain only objects'):
            _load_file(file_path)

    def test_load_json_primitive_raises(self, tmp_path: Path) -> None:
        """JSON primitive (not object/array) raises ValueError."""
        file_path = tmp_path / 'bad.json'
        file_path.write_text('"just a string"', encoding='utf-8')
        with pytest.raises(ValueError, match='must be object or array'):
            _load_file(file_path)

    def test_load_jsonl_valid(self, tmp_path: Path) -> None:
        """Load valid JSONL file."""
        file_path = tmp_path / 'test.jsonl'
        file_path.write_text(
            json.dumps({'id': 1}) + '\n' + json.dumps({'id': 2}) + '\n',
            encoding='utf-8',
        )
        data = _load_file(file_path)
        assert data == [{'id': 1}, {'id': 2}]

    def test_load_jsonl_skips_blank_lines(self, tmp_path: Path) -> None:
        """JSONL loader skips blank lines."""
        file_path = tmp_path / 'test.jsonl'
        file_path.write_text(
            json.dumps({'id': 1}) + '\n\n' + json.dumps({'id': 2}) + '\n',
            encoding='utf-8',
        )
        data = _load_file(file_path)
        assert data == [{'id': 1}, {'id': 2}]

    def test_load_jsonl_invalid_line_raises(self, tmp_path: Path) -> None:
        """JSONL with invalid line raises ValueError."""
        file_path = tmp_path / 'bad.jsonl'
        file_path.write_text('{"id": 1}\n{invalid}\n', encoding='utf-8')
        with pytest.raises(ValueError, match='Invalid JSON at line 2'):
            _load_file(file_path)

    def test_load_jsonl_non_object_line_raises(self, tmp_path: Path) -> None:
        """JSONL with non-object line raises ValueError."""
        file_path = tmp_path / 'bad.jsonl'
        file_path.write_text('{"id": 1}\n123\n', encoding='utf-8')
        with pytest.raises(ValueError, match='line 2 .* must be object'):
            _load_file(file_path)

    def test_load_yaml_dict(self, tmp_path: Path) -> None:
        """Load valid YAML dict."""
        file_path = tmp_path / 'test.yaml'
        file_path.write_text('name: Alice\nage: 30', encoding='utf-8')
        data = _load_file(file_path)
        assert data == {'name': 'Alice', 'age': 30}

    def test_load_yaml_list(self, tmp_path: Path) -> None:
        """Load valid YAML list of dicts."""
        file_path = tmp_path / 'test.yaml'
        file_path.write_text('- id: 1\n- id: 2', encoding='utf-8')
        data = _load_file(file_path)
        assert data == [{'id': 1}, {'id': 2}]

    def test_load_yaml_invalid_syntax_raises(self, tmp_path: Path) -> None:
        """Invalid YAML syntax raises ValueError."""
        file_path = tmp_path / 'bad.yaml'
        file_path.write_text('{\ninvalid: yaml:\n', encoding='utf-8')
        with pytest.raises(ValueError, match='Invalid YAML'):
            _load_file(file_path)

    def test_load_yaml_primitive_raises(self, tmp_path: Path) -> None:
        """YAML primitive (not object/array) raises ValueError."""
        file_path = tmp_path / 'bad.yaml'
        file_path.write_text('just a string', encoding='utf-8')
        with pytest.raises(ValueError, match='must be object or array'):
            _load_file(file_path)

    def test_load_unsupported_extension_raises(self, tmp_path: Path) -> None:
        """Unsupported file extension raises ValueError."""
        file_path = tmp_path / 'test.txt'
        file_path.write_text('hello', encoding='utf-8')
        with pytest.raises(ValueError, match='Unsupported file format: .txt'):
            _load_file(file_path)


class TestInferSchema:
    """Test infer_schema for JSON Schema generation."""

    def test_infer_schema_simple_types(self) -> None:
        """Infer schema from dict with simple types."""
        data = {
            'name': 'Alice',
            'age': 30,
            'active': True,
            'score': 95.5,
            'notes': None,
        }
        schema = infer_schema(data)
        assert schema == {
            'type': 'object',
            'properties': {
                'name': {'type': 'string'},
                'age': {'type': 'integer'},
                'active': {'type': 'boolean'},
                'score': {'type': 'number'},
                'notes': {'type': 'string'},  # None → string
            },
        }

    def test_infer_schema_nested_dict(self) -> None:
        """Infer schema from nested dict."""
        data = {
            'user': {
                'name': 'Bob',
                'id': 123,
            },
        }
        schema = infer_schema(data)
        assert schema == {
            'type': 'object',
            'properties': {
                'user': {
                    'type': 'object',
                    'properties': {
                        'name': {'type': 'string'},
                        'id': {'type': 'integer'},
                    },
                },
            },
        }

    def test_infer_schema_list_with_items(self) -> None:
        """Infer schema from dict with list (items inferred from first element)."""
        data = {
            'tags': ['python', 'testing'],
        }
        schema = infer_schema(data)
        assert schema == {
            'type': 'object',
            'properties': {
                'tags': {
                    'type': 'array',
                    'items': {'type': 'string'},
                },
            },
        }

    def test_infer_schema_empty_list(self) -> None:
        """Infer schema from dict with empty list (no items constraint)."""
        data = {'tags': []}
        schema = infer_schema(data)
        assert schema == {
            'type': 'object',
            'properties': {
                'tags': {'type': 'array'},
            },
        }

    def test_infer_schema_list_of_dicts(self) -> None:
        """Infer schema from list of dicts (array items are objects)."""
        data = {
            'users': [
                {'name': 'Alice', 'age': 30},
            ],
        }
        schema = infer_schema(data)
        assert schema == {
            'type': 'object',
            'properties': {
                'users': {
                    'type': 'array',
                    'items': {
                        'type': 'object',
                        'properties': {
                            'name': {'type': 'string'},
                            'age': {'type': 'integer'},
                        },
                    },
                },
            },
        }

    def test_infer_schema_empty_dict(self) -> None:
        """Infer schema from empty dict."""
        data = {}
        schema = infer_schema(data)
        assert schema == {
            'type': 'object',
            'properties': {},
        }

    def test_infer_schema_bool_before_int(self) -> None:
        """Ensure bool is detected before int (bool is subclass of int)."""
        data = {
            'flag': True,
            'count': 5,
        }
        schema = infer_schema(data)
        assert schema['properties']['flag'] == {'type': 'boolean'}
        assert schema['properties']['count'] == {'type': 'integer'}


class TestInferSchemaFromExample:
    """Test strict schema inference from YAML example text."""

    def test_infer_top_level_array(self) -> None:
        example = """
- name: 褒姒 # 角色名
  prompt: 古风半身像 # 图像提示词
""".strip()
        schema = infer_schema_from_example(example)
        assert schema == {
            'type': 'array',
            'items': {
                'type': 'object',
                'properties': {
                    'name': {'type': 'string', 'description': '角色名'},
                    'prompt': {'type': 'string', 'description': '图像提示词'},
                },
                'required': ['name', 'prompt'],
                'additionalProperties': False,
            },
        }

    def test_infer_object_array_comments_and_required(self) -> None:
        example = """
name: Alice # user name
status: active # status
status__enum:
  - active
  - disabled
tags: # labels
  - red # label value
users: # user list
  - name: Bob # person name
    age: 20 # person age
""".strip()
        schema = infer_schema_from_example(example)
        assert schema == {
            'type': 'object',
            'properties': {
                'name': {'type': 'string', 'description': 'user name'},
                'status': {
                    'type': 'string',
                    'description': 'status',
                    'enum': ['active', 'disabled'],
                },
                'tags': {
                    'type': 'array',
                    'items': {'type': 'string', 'description': 'label value'},
                    'description': 'labels',
                },
                'users': {
                    'type': 'array',
                    'items': {
                        'type': 'object',
                        'properties': {
                            'name': {'type': 'string', 'description': 'person name'},
                            'age': {'type': 'integer', 'description': 'person age'},
                        },
                        'required': ['name', 'age'],
                        'additionalProperties': False,
                    },
                    'description': 'user list',
                },
            },
            'required': ['name', 'status', 'tags', 'users'],
            'additionalProperties': False,
        }

    def test_union_like_array_rejected(self) -> None:
        example = """
items:
  - 1
  - text
""".strip()
        with pytest.raises(ValueError, match='Array items must share the same schema'):
            infer_schema_from_example(example)

    def test_null_rejected(self) -> None:
        with pytest.raises(ValueError, match='Null values are not supported'):
            infer_schema_from_example('name: null')

    def test_flow_style_collection_rejected(self) -> None:
        with pytest.raises(ValueError, match='Flow-style YAML collections are not supported'):
            infer_schema_from_example('tags: []')

    def test_flow_style_object_rejected(self) -> None:
        with pytest.raises(ValueError, match='Flow-style YAML collections are not supported'):
            infer_schema_from_example('user: {}')

    def test_enum_value_must_be_member(self) -> None:
        example = """
status: active
status__enum:
  - disabled
  - pending
""".strip()
        with pytest.raises(ValueError, match='is not declared in enum'):
            infer_schema_from_example(example)

    def test_array_scalar_with_colon_is_not_misparsed_as_mapping(self) -> None:
        example = """
images:
  - https://example.com/a.png # image url
""".strip()
        schema = infer_schema_from_example(example)
        assert schema['properties']['images']['items']['type'] == 'string'
        assert schema['properties']['images']['items']['description'] == 'image url'

    def test_comment_conflict_rejected_for_array_items(self) -> None:
        example = """
tags:
  - red # first
  - blue # second
""".strip()
        with pytest.raises(ValueError, match='Conflicting comments'):
            infer_schema_from_example(example)


class TestResolveInputDefaults:
    """Test resolve_input with defaults parameter."""

    def test_defaults_merged_into_single_dict(self, tmp_path: Path) -> None:
        """Defaults are merged into a single dict record."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        defaults = {'language': 'Chinese', 'style': 'formal'}
        records = resolve_input({'name': 'Alice'}, prompt_file, defaults=defaults)
        assert records == [{'language': 'Chinese', 'style': 'formal', 'name': 'Alice'}]

    def test_defaults_merged_into_batch_list(self, tmp_path: Path) -> None:
        """Defaults are merged into every record in batch mode."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        defaults = {'language': 'Chinese'}
        data = [{'name': 'Alice'}, {'name': 'Bob'}]
        records = resolve_input(data, prompt_file, defaults=defaults)
        assert records == [
            {'language': 'Chinese', 'name': 'Alice'},
            {'language': 'Chinese', 'name': 'Bob'},
        ]

    def test_data_overrides_defaults(self, tmp_path: Path) -> None:
        """Per-record data values override defaults when keys conflict."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        defaults = {'language': 'Chinese', 'tone': 'formal'}
        data = [{'name': 'Alice', 'tone': 'casual'}]
        records = resolve_input(data, prompt_file, defaults=defaults)
        assert records == [{'language': 'Chinese', 'tone': 'casual', 'name': 'Alice'}]

    def test_defaults_none_has_no_effect(self, tmp_path: Path) -> None:
        """None defaults has no effect (backward compatible)."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input(SAMPLE_DICT, prompt_file, defaults=None)
        assert records == [SAMPLE_DICT]

    def test_defaults_empty_dict_has_no_effect(self, tmp_path: Path) -> None:
        """Empty defaults dict has no effect."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input(SAMPLE_DICT, prompt_file, defaults={})
        assert records == [SAMPLE_DICT]

    def test_defaults_with_none_input(self, tmp_path: Path) -> None:
        """Defaults with None input returns empty (no records to merge into)."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_input(None, prompt_file, defaults={'language': 'Chinese'})
        assert records == []


class TestResolveFilesSinglePath:
    """Test resolve_files with a single file path string."""

    def test_json_file_single_dict(self, tmp_path: Path) -> None:
        """Single JSON file with dict returns [dict]."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'input.json'
        input_file.write_text(json.dumps(SAMPLE_DICT), encoding='utf-8')

        records = resolve_files('input.json', prompt_file)
        assert records == [SAMPLE_DICT]

    def test_json_file_list_of_dicts(self, tmp_path: Path) -> None:
        """JSON file with list[dict] returns list."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'input.json'
        input_file.write_text(json.dumps(SAMPLE_LIST), encoding='utf-8')

        records = resolve_files('input.json', prompt_file)
        assert records == SAMPLE_LIST

    def test_jsonl_file(self, tmp_path: Path) -> None:
        """JSONL file returns list of dicts."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'input.jsonl'
        input_file.write_text(
            json.dumps({'id': 1}) + '\n' + json.dumps({'id': 2}),
            encoding='utf-8',
        )

        records = resolve_files('input.jsonl', prompt_file)
        assert records == [{'id': 1}, {'id': 2}]

    def test_yaml_file(self, tmp_path: Path) -> None:
        """YAML file with dict returns [dict]."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'input.yaml'
        input_file.write_text('name: Bob\nage: 25', encoding='utf-8')

        records = resolve_files('input.yaml', prompt_file)
        assert records == [{'name': 'Bob', 'age': 25}]

    def test_file_not_found_raises(self, tmp_path: Path) -> None:
        """Non-existent file raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='File not found'):
            resolve_files('nonexistent.json', prompt_file)


class TestResolveFilesListPath:
    """Test resolve_files with a list of file paths."""

    def test_multiple_files_concatenated(self, tmp_path: Path) -> None:
        """Multiple file paths are loaded and concatenated."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        file1 = tmp_path / 'input1.json'
        file1.write_text(json.dumps({'id': 1}), encoding='utf-8')
        file2 = tmp_path / 'input2.json'
        file2.write_text(json.dumps([{'id': 2}, {'id': 3}]), encoding='utf-8')

        records = resolve_files(['input1.json', 'input2.json'], prompt_file)
        assert records == [{'id': 1}, {'id': 2}, {'id': 3}]

    def test_empty_list_returns_empty(self, tmp_path: Path) -> None:
        """Empty list returns []."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records = resolve_files([], prompt_file)
        assert records == []


class TestResolveFilesDirectoryPath:
    """Test resolve_files with directory paths."""

    def test_directory_json_files_concatenated(self, tmp_path: Path) -> None:
        """Top-level JSON files in a directory are loaded in name order."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'b.json').write_text(json.dumps([{'id': 2}, {'id': 3}]), encoding='utf-8')
        (data_dir / 'a.json').write_text(json.dumps({'id': 1}), encoding='utf-8')

        records = resolve_files('data', prompt_file)
        assert records == [{'id': 1}, {'id': 2}, {'id': 3}]

    def test_directory_mixed_json_yaml_raises(self, tmp_path: Path) -> None:
        """Mixed suffixes in one directory raise ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'a.json').write_text(json.dumps({'id': 1}), encoding='utf-8')
        (data_dir / 'b.yaml').write_text('id: 2\n', encoding='utf-8')

        with pytest.raises(ValueError, match='same file suffix'):
            resolve_files('data', prompt_file)

    def test_directory_mixed_yaml_yml_raises(self, tmp_path: Path) -> None:
        """Mixing .yaml and .yml in one directory is not allowed."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'a.yaml').write_text('id: 1\n', encoding='utf-8')
        (data_dir / 'b.yml').write_text('id: 2\n', encoding='utf-8')

        with pytest.raises(ValueError, match='same file suffix'):
            resolve_files('data', prompt_file)

    def test_directory_with_unsupported_file_raises(self, tmp_path: Path) -> None:
        """Unsupported file extensions in directory should raise."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'a.json').write_text(json.dumps({'id': 1}), encoding='utf-8')
        (data_dir / 'readme.md').write_text('# note', encoding='utf-8')

        with pytest.raises(ValueError, match='Unsupported file format in directory'):
            resolve_files('data', prompt_file)

    def test_empty_directory_raises(self, tmp_path: Path) -> None:
        """Empty directory should raise ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()

        with pytest.raises(ValueError, match='Directory is empty'):
            resolve_files('data', prompt_file)

    def test_directory_scan_is_non_recursive(self, tmp_path: Path) -> None:
        """Nested files are ignored when loading a directory path."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'a.json').write_text(json.dumps({'id': 1}), encoding='utf-8')

        nested = data_dir / 'nested'
        nested.mkdir()
        (nested / 'b.yaml').write_text('id: 2\n', encoding='utf-8')

        records = resolve_files('data', prompt_file)
        assert records == [{'id': 1}]


class TestResolveFilesDirectoryInListPath:
    """Test resolve_files with mixed directory and file paths in lists."""

    def test_list_with_directory_and_file_same_suffix(self, tmp_path: Path) -> None:
        """Directory + file with same suffix should pass."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'b.json').write_text(json.dumps({'id': 2}), encoding='utf-8')
        (data_dir / 'c.json').write_text(json.dumps({'id': 3}), encoding='utf-8')
        (tmp_path / 'a.json').write_text(json.dumps({'id': 1}), encoding='utf-8')

        records = resolve_files(['data', 'a.json'], prompt_file)
        assert records == [{'id': 2}, {'id': 3}, {'id': 1}]

    def test_list_with_directory_and_file_different_suffix_raises(self, tmp_path: Path) -> None:
        """Directory + file with different suffixes should fail globally."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        data_dir = tmp_path / 'data'
        data_dir.mkdir()
        (data_dir / 'a.json').write_text(json.dumps({'id': 1}), encoding='utf-8')
        (tmp_path / 'b.yaml').write_text('id: 2\n', encoding='utf-8')

        with pytest.raises(ValueError, match='same file suffix'):
            resolve_files(['data', 'b.yaml'], prompt_file)


class TestResolveFilesInvalidConfig:
    """Test resolve_files rejects unsupported config types."""

    def test_dict_config_raises(self, tmp_path: Path) -> None:
        """files config dict is not supported."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='Unsupported files config type: dict'):
            resolve_files({'glob': 'data/*.json'}, prompt_file)


class TestResolveFilesDefaults:
    """Test resolve_files with defaults parameter."""

    def test_defaults_merged_into_single_file(self, tmp_path: Path) -> None:
        """Defaults are merged into records loaded from a single file."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'data.jsonl'
        input_file.write_text(
            json.dumps({'name': 'Alice'}) + '\n' + json.dumps({'name': 'Bob'}) + '\n',
            encoding='utf-8',
        )
        defaults = {'language': 'Chinese'}
        records = resolve_files('data.jsonl', prompt_file, defaults=defaults)
        assert records == [
            {'language': 'Chinese', 'name': 'Alice'},
            {'language': 'Chinese', 'name': 'Bob'},
        ]

    def test_defaults_merged_into_multiple_files(self, tmp_path: Path) -> None:
        """Defaults are merged into records loaded from multiple files."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        (tmp_path / 'a.json').write_text(json.dumps({'name': 'Alice'}), encoding='utf-8')
        (tmp_path / 'b.json').write_text(json.dumps({'name': 'Bob'}), encoding='utf-8')
        records = resolve_files(['a.json', 'b.json'], prompt_file, defaults={'language': 'Chinese'})
        assert records == [
            {'language': 'Chinese', 'name': 'Alice'},
            {'language': 'Chinese', 'name': 'Bob'},
        ]

    def test_record_overrides_defaults(self, tmp_path: Path) -> None:
        """Per-record values override defaults when keys conflict."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'data.json'
        input_file.write_text(json.dumps({'name': 'Alice', 'tone': 'casual'}), encoding='utf-8')

        defaults = {'tone': 'formal', 'language': 'Chinese'}
        records = resolve_files('data.json', prompt_file, defaults=defaults)
        assert records == [{'language': 'Chinese', 'tone': 'casual', 'name': 'Alice'}]


class TestPromptInputConfigValidation:
    """Test PromptInputConfig mutual exclusion and files parsing."""

    def test_data_and_files_mutually_exclusive(self) -> None:
        """Setting both data and files raises ValueError."""
        from dotpromptz.typing import PromptInputConfig

        with pytest.raises(ValueError, match='mutually exclusive'):
            PromptInputConfig(data={'name': 'Alice'}, files='data.json')

    def test_files_rejects_dict(self) -> None:
        """PromptInputConfig.files rejects dict-style config."""
        from dotpromptz.typing import PromptInputConfig

        with pytest.raises(ValidationError):
            PromptInputConfig(files={'glob': 'data/*.json'})

    def test_files_accepts_string(self) -> None:
        """PromptInputConfig.files accepts a plain string."""
        from dotpromptz.typing import PromptInputConfig

        cfg = PromptInputConfig(files='data/users.json')
        assert cfg.files == 'data/users.json'
        assert cfg.data is None

    def test_files_accepts_list(self) -> None:
        """PromptInputConfig.files accepts a list of strings."""
        from dotpromptz.typing import PromptInputConfig

        cfg = PromptInputConfig(files=['a.json', 'b.json'])
        assert cfg.files == ['a.json', 'b.json']
