"""Tests for input_resolver module."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from dotpromptz.input_resolver import (
    _load_file,
    _validate_file_path,
    infer_schema,
    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 and False (single mode)."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        records, is_batch = resolve_input(None, prompt_file)
        assert records == []
        assert is_batch is False

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


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

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

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

    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='List must contain only dicts or only strings'):
            resolve_input([{'a': 1}, 123], prompt_file)

    def test_list_of_strings_loads_files_batch_mode(self, tmp_path: Path) -> None:
        """List[str] loads multiple files and returns concatenated batch."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()

        # Create two JSON files
        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, is_batch = resolve_input(['input1.json', 'input2.json'], prompt_file)
        assert records == [{'id': 1}, {'id': 2}, {'id': 3}]
        assert is_batch is True

    def test_list_mixed_string_and_dict_raises(self, tmp_path: Path) -> None:
        """List with mixed types (str and dict) raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='List must contain only dicts or only strings'):
            resolve_input(['file.json', {'a': 1}], 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='List must contain dicts or strings, not int'):
            resolve_input([123, 456], prompt_file)


class TestResolveInputString:
    """Test resolve_input with string file path inputs."""

    def test_json_file_with_dict_returns_single_mode(self, tmp_path: Path) -> None:
        """JSON file with dict returns [dict] and False (single mode)."""
        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, is_batch = resolve_input('input.json', prompt_file)
        assert records == [SAMPLE_DICT]
        assert is_batch is False

    def test_json_file_with_list_returns_batch_mode(self, tmp_path: Path) -> None:
        """JSON file with list returns list and True (batch mode)."""
        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, is_batch = resolve_input('input.json', prompt_file)
        assert records == SAMPLE_LIST
        assert is_batch is True

    def test_jsonl_file_always_returns_batch_mode(self, tmp_path: Path) -> None:
        """JSONL file always returns batch mode (even single line)."""
        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, is_batch = resolve_input('input.jsonl', prompt_file)
        assert records == [{'id': 1}, {'id': 2}]
        assert is_batch is True

    def test_yaml_file_with_dict_returns_single_mode(self, tmp_path: Path) -> None:
        """YAML file with dict returns [dict] and False (single mode)."""
        pytest.importorskip('yaml')
        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, is_batch = resolve_input('input.yaml', prompt_file)
        assert records == [{'name': 'Bob', 'age': 25}]
        assert is_batch is False

    def test_yaml_file_with_list_returns_batch_mode(self, tmp_path: Path) -> None:
        """YAML file with list returns list and True (batch mode)."""
        pytest.importorskip('yaml')
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        input_file = tmp_path / 'input.yaml'
        input_file.write_text('- id: 1\n- id: 2', encoding='utf-8')

        records, is_batch = resolve_input('input.yaml', prompt_file)
        assert records == [{'id': 1}, {'id': 2}]
        assert is_batch is True


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

    def test_bool_input_raises(self, tmp_path: Path) -> None:
        """Bool input raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='Unsupported input type: bool'):
            resolve_input(True, prompt_file)

    def test_int_input_raises(self, tmp_path: Path) -> None:
        """Int input raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='Unsupported input type: int'):
            resolve_input(42, prompt_file)

    def test_float_input_raises(self, tmp_path: Path) -> None:
        """Float input raises ValueError."""
        prompt_file = tmp_path / 'test.prompt'
        prompt_file.touch()
        with pytest.raises(ValueError, match='Unsupported input type: float'):
            resolve_input(3.14, prompt_file)


class TestValidateFilePath:
    """Test _validate_file_path security 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_path_traversal_attack_raises(self, tmp_path: Path) -> None:
        """Path traversal attack (../../etc/passwd) raises ValueError."""
        prompt_dir = tmp_path / 'prompts'
        prompt_dir.mkdir()
        parent_file = tmp_path / 'secret.txt'
        parent_file.touch()

        with pytest.raises(ValueError, match='resolves outside prompt directory'):
            _validate_file_path('../../secret.txt', prompt_dir)

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

        with pytest.raises(ValueError, match='resolves outside prompt directory'):
            _validate_file_path(str(outside_file.resolve()), prompt_dir)

    def test_missing_file_raises(self, tmp_path: Path) -> None:
        """Non-existent file raises ValueError."""
        prompt_dir = tmp_path
        with pytest.raises(ValueError, match='File not found'):
            _validate_file_path('missing.json', prompt_dir)


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."""
        pytest.importorskip('yaml')
        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."""
        pytest.importorskip('yaml')
        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."""
        pytest.importorskip('yaml')
        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."""
        pytest.importorskip('yaml')
        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'}
