import base64
from pathlib import Path

import pytest

import dotpromptz
from dotpromptz import build_prompt, compile_prompt, plan_prompt, render_compiled_prompt
from dotpromptz.errors import InputResolutionError
from dotpromptz.typing import MediaPart, TextPart
from dotpromptz.version import CURRENT_MAJOR
from dotpromptz.yamlio import load_yaml


def _write_prompt(path: Path, content: str) -> Path:
    path.write_text(content, encoding='utf-8')
    return path


def test_public_api_exposes_new_services_only() -> None:
    assert hasattr(dotpromptz, 'compile_prompt')
    assert hasattr(dotpromptz, 'plan_prompt')
    assert hasattr(dotpromptz, 'build_prompt')

    assert not hasattr(dotpromptz, 'PromptEngine')
    assert not hasattr(dotpromptz, 'run')
    assert not hasattr(dotpromptz, 'resolve_adapter')


def test_compile_and_plan_prompt_pipeline(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'sample.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  file_name: "result_{{{{name}}}}"
input:
  data:
    name: Alice
---
:::user
Hello {{{{name}}}}
""",
    )

    compiled = compile_prompt(prompt_file)
    rendered = render_compiled_prompt(compiled, compiled.input_list[0])
    plan = plan_prompt(compiled, batch_time='20260101_000000')

    assert compiled.prompt_path == prompt_file
    assert rendered.messages[0].role == 'user'
    assert len(plan.items) == 1
    assert plan.items[0].output_stem == 'result_Alice'
    assert plan.items[0].output_path.name == 'result_Alice.json'


def test_build_prompt_emits_yaml_output(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'build.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  file_name: "demo"
---
:::user
hello
""",
    )
    report = build_prompt(prompt_file, log_dir=tmp_path)
    assert report.output_file.exists()
    assert report.rendered.messages[0].role == 'user'
    data = load_yaml(report.output_file.read_text(encoding='utf-8'))
    assert data['messages'][0]['role'] == 'user'


def test_render_compiled_prompt_accepts_output_example(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'example.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: yaml
  example: |
    name: 褒姒
    prompt: 实际的提示词
  output_dir: "output/{{{{TIME}}}}"
  file_name: "{{{{batch_id}}}}"
input:
  data:
    batch_id: run_001
    name: 褒姒
---
:::user
人物列表：{{{{name}}}}
""",
    )

    compiled = compile_prompt(prompt_file)
    rendered = render_compiled_prompt(compiled, compiled.input_list[0])

    assert rendered.output is not None
    assert rendered.output.example is not None
    assert rendered.output.schema_value is not None
    assert rendered.output.file_name == 'run_001'
    assert rendered.output.output_dir is not None
    assert rendered.output.output_dir.startswith('output/')
    assert rendered.output.schema_value['required'] == ['name', 'prompt']


def test_plan_prompt_uses_png_extension_for_image_output(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'image_out.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: image
  file_name: "image_{{{{name}}}}"
input:
  data:
    name: Alice
---
:::user
draw a cat
""",
    )
    compiled = compile_prompt(prompt_file)
    plan = plan_prompt(compiled, batch_time='20260101_000000')
    assert plan.items[0].output_path.name == 'image_Alice.png'


def test_plan_prompt_increments_mumber_for_existing_file(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'mumber_file.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  file_name: "result_{{{{NUMBER}}}}"
---
:::user
Current slot {{{{NUMBER}}}}
""",
    )
    (tmp_path / 'output' / 'mumber_file_20260101_000000').mkdir(parents=True, exist_ok=True)
    (tmp_path / 'output' / 'mumber_file_20260101_000000' / 'result_1.json').write_text('{}', encoding='utf-8')

    compiled = compile_prompt(prompt_file)
    plan = plan_prompt(compiled, batch_time='20260101_000000')

    message_part = plan.items[0].rendered.messages[0].content[0]
    assert isinstance(message_part, TextPart)
    assert plan.items[0].output_stem == 'result_2'
    assert plan.items[0].output_path.name == 'result_2.json'
    assert message_part.text == 'Current slot 2'


def test_plan_prompt_increments_mumber_for_existing_output_dir(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'mumber_dir.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  output_dir: "output/run_{{{{NUMBER}}}}"
  file_name: "result"
---
:::user
Dir slot {{{{NUMBER}}}}
""",
    )

    compiled = compile_prompt(prompt_file)
    plan = plan_prompt(compiled, batch_time='20260101_000000')

    message_part = plan.items[0].rendered.messages[0].content[0]
    assert isinstance(message_part, TextPart)
    assert plan.output_dir == tmp_path / 'output' / 'run_1'
    assert plan.items[0].output_path == tmp_path / 'output' / 'run_1' / 'result.json'
    assert message_part.text == 'Dir slot 1'


def test_plan_prompt_increments_mumber_for_existing_output_file_in_numbered_dir(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'mumber_dir.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  output_dir: "output/run_{{{{NUMBER}}}}"
  file_name: "result"
---
:::user
Dir slot {{{{NUMBER}}}}
""",
    )
    (tmp_path / 'output' / 'run_1').mkdir(parents=True, exist_ok=True)
    (tmp_path / 'output' / 'run_1' / 'result.json').write_text('{}', encoding='utf-8')

    compiled = compile_prompt(prompt_file)
    plan = plan_prompt(compiled, batch_time='20260101_000000')

    message_part = plan.items[0].rendered.messages[0].content[0]
    assert isinstance(message_part, TextPart)
    assert plan.output_dir == tmp_path / 'output' / 'run_2'
    assert plan.items[0].output_path == tmp_path / 'output' / 'run_2' / 'result.json'
    assert message_part.text == 'Dir slot 2'


def test_plan_prompt_increments_mumber_within_batch_duplicates(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'mumber_batch.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  file_name: "dup_{{{{NUMBER}}}}"
input:
  data:
    - name: Alice
    - name: Bob
---
:::user
Batch slot {{{{NUMBER}}}} for {{{{name}}}}
""",
    )

    compiled = compile_prompt(prompt_file)
    plan = plan_prompt(compiled, batch_time='20260101_000000')

    rendered_texts = []
    for item in plan.items:
        part = item.rendered.messages[0].content[0]
        assert isinstance(part, TextPart)
        rendered_texts.append(part.text)

    assert [item.output_stem for item in plan.items] == ['dup_1', 'dup_2']
    assert rendered_texts == [
        'Batch slot 1 for Alice',
        'Batch slot 2 for Bob',
    ]


def test_render_compiled_prompt_resolves_relative_image_path(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'image.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  file_name: "demo"
---
:::user
describe this image
:::image
assets/red.png
""",
    )
    image_path = tmp_path / 'assets' / 'red.png'
    image_path.parent.mkdir(parents=True, exist_ok=True)
    image_path.write_bytes(
        base64.b64decode(
            'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg=='
        )
    )

    compiled = compile_prompt(prompt_file)
    rendered = render_compiled_prompt(compiled, compiled.input_list[0])

    media_part = rendered.messages[0].content[1]
    assert isinstance(media_part, MediaPart)
    assert media_part.media.url == str(image_path.resolve())


def test_render_compiled_prompt_rejects_data_uri_image(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'data_uri.prompt',
        f"""---
version: {CURRENT_MAJOR}
adapter: openai
output:
  format: json
  file_name: "demo"
---
:::user
describe this image
:::image
data:image/png;base64,abc123
""",
    )
    compiled = compile_prompt(prompt_file)
    with pytest.raises(ValueError, match='data: image URLs are not supported'):
        render_compiled_prompt(compiled, compiled.input_list[0])


def test_compile_prompt_chain_json_uses_parent_output_as_input(tmp_path: Path) -> None:
    parent_output = tmp_path / 'parent.json'
    parent_output.write_text('{"topic":"AI","count":2}', encoding='utf-8')
    prompt_file = _write_prompt(
        tmp_path / 'chain.prompt',
        f"""---
version: {CURRENT_MAJOR}
input:
  chain: true
  defaults:
    lang: zh
---
:::user
{{{{ topic }}}} {{{{ count }}}} {{{{ lang }}}}
""",
    )

    compiled = compile_prompt(prompt_file, chain_source=parent_output)

    assert compiled.input_list == [{'lang': 'zh', 'topic': 'AI', 'count': 2}]


def test_render_compiled_prompt_chain_txt_injects_text_auto_var(tmp_path: Path) -> None:
    chain_file = tmp_path / 'parent.txt'
    chain_file.write_text('hello from parent', encoding='utf-8')
    prompt_file = _write_prompt(
        tmp_path / 'child.prompt',
        f"""---
version: {CURRENT_MAJOR}
input:
  chain: true
---
:::user
{{{{ TEXT }}}}
""",
    )

    compiled = compile_prompt(prompt_file, chain_source=chain_file)
    rendered = render_compiled_prompt(compiled, compiled.input_list[0])

    part = rendered.messages[0].content[0]
    assert isinstance(part, TextPart)
    assert part.text == 'hello from parent'


def test_compile_prompt_chain_without_runtime_source_raises(tmp_path: Path) -> None:
    prompt_file = _write_prompt(
        tmp_path / 'missing_chain.prompt',
        f"""---
version: {CURRENT_MAJOR}
input:
  chain: true
---
:::user
hello
""",
    )

    with pytest.raises(InputResolutionError, match='runtime-provided chain source'):
        compile_prompt(prompt_file)


def test_compile_prompt_chain_rejects_image_source(tmp_path: Path) -> None:
    chain_file = tmp_path / 'parent.png'
    chain_file.write_bytes(b'\x89PNG')
    prompt_file = _write_prompt(
        tmp_path / 'image_chain.prompt',
        f"""---
version: {CURRENT_MAJOR}
input:
  chain: true
---
:::user
hello
""",
    )

    with pytest.raises(InputResolutionError, match='image outputs'):
        compile_prompt(prompt_file, chain_source=chain_file)
