"""Tests for service-layer prompt build/run APIs."""

import asyncio
from pathlib import Path
from textwrap import dedent

import pytest

from dotpromptz.adapters._base import GenerateResponse
from dotpromptz.errors import (
    ExecutionConfigError,
    InputResolutionError,
    InputVariableNamingError,
    TemplateMissingVariableError,
)
from dotpromptz.service import build_prompt, run_prompt
from dotpromptz.typing import RenderedPrompt
from dotpromptz.version import CURRENT_MAJOR


def test_build_missing_file_raises() -> None:
    with pytest.raises(InputResolutionError, match='Path does not exist'):
        build_prompt('/not/exist.prompt', log_dir=Path('/tmp'))


def test_build_resolves_inline_data_and_writes_yaml(tmp_path: Path) -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        input:
          data:
            topic: AI
        ---
        :::user
        Hello {{{{ topic }}}}
    """
    ).strip()
    prompt_file = tmp_path / 'a.prompt'
    prompt_file.write_text(source, encoding='utf-8')

    report = build_prompt(prompt_file, log_dir=tmp_path)
    assert report.compiled.input_list == [{'topic': 'AI'}]
    assert report.output_file.exists()
    assert report.output_file.suffix == '.yaml'


def test_run_requires_output_config(tmp_path: Path) -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        ---
        :::user
        hello
    """
    ).strip()
    prompt_file = tmp_path / 'c.prompt'
    prompt_file.write_text(source, encoding='utf-8')

    with pytest.raises(ExecutionConfigError, match='No output configuration'):
        asyncio.run(run_prompt(prompt_file, log_dir=tmp_path))


def test_build_raises_when_template_input_missing(tmp_path: Path) -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        ---
        :::user
        hello {{{{ topic }}}}
        """
    ).strip()
    prompt_file = tmp_path / 'missing.prompt'
    prompt_file.write_text(source, encoding='utf-8')

    with pytest.raises(TemplateMissingVariableError, match='topic'):
        build_prompt(prompt_file, log_dir=tmp_path)


def test_build_rejects_uppercase_input_key(tmp_path: Path) -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        input:
          data:
            TIME: "20250101_120000"
        ---
        :::user
        hi
        """
    ).strip()
    prompt_file = tmp_path / 'reserved.prompt'
    prompt_file.write_text(source, encoding='utf-8')

    with pytest.raises(InputVariableNamingError, match='TIME'):
        build_prompt(prompt_file, log_dir=tmp_path)


def test_run_prompt_executes_runtime_next_per_item(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    root_prompt = tmp_path / 'root.prompt'
    child_prompt = tmp_path / 'child.prompt'

    root_prompt.write_text(
        dedent(
            f"""
            ---
            version: {CURRENT_MAJOR}
            adapter: openai
            output:
              format: json
              file_name: "{{{{ name }}}}"
            runtime:
              max_workers: 1
              next: ./child.prompt
            input:
              data:
                - name: first
                - name: second
            ---
            :::user
            root {{{{ name }}}}
            """
        ).strip(),
        encoding='utf-8',
    )
    child_prompt.write_text(
        dedent(
            f"""
            ---
            version: {CURRENT_MAJOR}
            adapter: openai
            input:
              chain: true
            output:
              format: txt
              file_name: "child_{{{{ value }}}}"
            ---
            :::user
            child {{{{ value }}}}
            """
        ).strip(),
        encoding='utf-8',
    )

    events: list[str] = []

    class _FakeAdapter:
        async def generate(self, rendered: RenderedPrompt, **kwargs: object) -> GenerateResponse:
            text = rendered.messages[0].content[0].text
            events.append(f'start:{text}')
            if text == 'root first':
                await asyncio.sleep(0.01)
                events.append('end:root first')
                return GenerateResponse(text='{"value":"first"}')
            if text == 'root second':
                await asyncio.sleep(0.05)
                events.append('end:root second')
                return GenerateResponse(text='{"value":"second"}')
            events.append(f'end:{text}')
            return GenerateResponse(text=f'done {text}')

    monkeypatch.setattr('dotpromptz.executor.resolve_batch_adapter', lambda rendered: _FakeAdapter())

    report = asyncio.run(run_prompt(root_prompt, log_dir=tmp_path))

    assert len(report.batch_results) == 4
    assert [item.trace for item in report.batch_results] == [(0,), (0, 0), (1,), (1, 0)]
    assert any(path.name == 'first.json' for path in report.written_files)
    assert any(path.name == 'second.json' for path in report.written_files)
    assert any(path.name == 'child_first.txt' for path in report.written_files)
    assert any(path.name == 'child_second.txt' for path in report.written_files)
    assert events.index('start:child first') < events.index('end:root second')
