"""Tests for strict parser semantics."""

from textwrap import dedent

import pytest

from dotpromptz.blocks import parse_blocks_to_messages
from dotpromptz.errors import (
    FrontmatterMissingError,
    FrontmatterValidationError,
    MessageBlockParseError,
    PromptVersionError,
)
from dotpromptz.parser import extract_frontmatter_and_body, parse_document
from dotpromptz.typing import MediaPart, Role, TextPart
from dotpromptz.version import CURRENT_MAJOR


def test_extract_frontmatter_and_body_strict() -> None:
    source = '---\nversion: 1\n---\n:::user\nhello'
    frontmatter, body = extract_frontmatter_and_body(source)
    assert 'version: 1' in frontmatter
    assert body.startswith(':::user')


def test_parse_document_requires_frontmatter() -> None:
    with pytest.raises(FrontmatterMissingError, match='Frontmatter is required'):
        parse_document(':::user\nhello')


def test_parse_document_requires_version_match() -> None:
    source = dedent(
        """
        ---
        version: 999
        ---
        :::user
        hello
        """
    ).strip()
    with pytest.raises(PromptVersionError):
        parse_document(source)


def test_parse_document_with_valid_frontmatter() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        adapter: openai
        config:
          model: gpt-4o
        output:
          format: txt
          file_name: result
        ---
        :::user
        hello
        """
    ).strip()
    result = parse_document(source)
    assert result.version == CURRENT_MAJOR
    assert result.adapter == 'openai'
    assert isinstance(result.config, dict)
    assert result.config.get('model') == 'gpt-4o'
    assert result.output is not None
    assert result.output.format == 'txt'
    assert ':::user' in result.template


def test_parse_document_accepts_input_chain() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        input:
          chain: true
          defaults:
            role: reviewer
        ---
        :::user
        hello
        """
    ).strip()

    result = parse_document(source)

    assert result.input is not None
    assert result.input.chain is True
    assert result.input.defaults == {'role': 'reviewer'}


def test_parse_document_rejects_input_chain_with_files() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        input:
          chain: true
          files: ./data.json
        ---
        :::user
        hello
        """
    ).strip()

    with pytest.raises(FrontmatterValidationError, match='mutually exclusive'):
        parse_document(source)


def test_parse_document_supports_image_output_format() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        output:
          format: image
          file_name: img
        ---
        :::user
        draw a cat
        """
    ).strip()
    result = parse_document(source)
    assert result.output is not None
    assert result.output.format == 'image'


def test_parse_document_accepts_minimal_image_config_fields() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        config:
          model: gpt-image-1.5
          resolution: 1024x1024
          aspect_ratio: 1:1
          transparent: true
        output:
          format: image
          file_name: img
        ---
        :::user
        draw a logo
        """
    ).strip()
    result = parse_document(source)
    assert isinstance(result.config, dict)
    assert result.config.get('resolution') == '1024x1024'
    assert result.config.get('aspect_ratio') == '1:1'
    assert result.config.get('transparent') is True


def test_parse_document_rejects_removed_image_params() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        config:
          model: gpt-image-1.5
          quality: high
        output:
          format: image
          file_name: img
        ---
        :::user
        draw a logo
        """
    ).strip()
    with pytest.raises(FrontmatterValidationError, match='Invalid config field'):
        parse_document(source)


def test_parse_document_with_output_example_infers_schema() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        output:
          format: json
          example: |
            name: Alice # 用户名
            age: 30 # 年龄
        ---
        :::user
        hello
        """
    ).strip()

    result = parse_document(source)
    assert result.output is not None
    assert result.output.schema_value is not None
    assert result.output.schema_value['required'] == ['name', 'age']
    assert result.output.schema_value['properties']['name']['description'] == '用户名'


def test_parse_document_with_output_example_list_infers_array_schema() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        output:
          format: yaml
          example: |
            - name: 褒姒
              prompt: 古风半身像
        ---
        :::user
        hello
        """
    ).strip()

    result = parse_document(source)
    assert result.output is not None
    assert result.output.schema_value is not None
    assert result.output.schema_value['type'] == 'array'
    assert result.output.schema_value['items']['required'] == ['name', 'prompt']


def test_parse_document_accepts_wrapped_output_schema_with_fixed_value() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        output:
          format: json
          schema:
            data:
              type: object
              properties:
                name:
                  type: string
              required: [name]
            fixed:
              name: Alice
        ---
        :::user
        hello
        """
    ).strip()

    result = parse_document(source)

    assert result.output is not None
    assert result.output.schema_value == {
        'type': 'object',
        'properties': {'name': {'type': 'string'}},
        'required': ['name'],
    }
    assert result.output.fixed == {'name': 'Alice'}


def test_parse_document_rejects_fixed_output_for_txt_format() -> None:
    source = dedent(
        f"""
        ---
        version: {CURRENT_MAJOR}
        output:
          format: txt
          schema:
            data:
              type: object
            fixed:
              text: hello
        ---
        :::user
        hello
        """
    ).strip()

    with pytest.raises(FrontmatterValidationError, match='only effective when output.format'):
        parse_document(source)


def test_parse_blocks_to_messages_without_blocks_defaults_to_single_user_message() -> None:
    result = parse_blocks_to_messages('hello world')
    assert len(result) == 1
    assert result[0].role == Role.USER
    assert result[0].content == [TextPart(text='hello world')]


def test_parse_blocks_to_messages_with_block_text_belongs_to_previous_block() -> None:
    rendered = dedent(
        """
        :::user
        first
        :::model
        second line 1
        second line 2
        """
    ).strip()

    result = parse_blocks_to_messages(rendered)
    assert len(result) == 2
    assert result[0].role == Role.USER
    assert result[1].role == Role.MODEL
    assert result[1].content == [TextPart(text='second line 1\nsecond line 2')]


def test_parse_blocks_to_messages_rejects_non_empty_text_before_first_block() -> None:
    rendered = 'prefix text\n:::user\nhello'
    with pytest.raises(MessageBlockParseError, match='first block'):
        parse_blocks_to_messages(rendered)


def test_parse_blocks_to_messages_supports_image_block() -> None:
    rendered = dedent(
        """
        :::user
        Look at this
        :::image
        https://example.com/cat.jpg
        """
    ).strip()

    result = parse_blocks_to_messages(rendered)
    assert len(result) == 1
    assert len(result[0].content) == 2
    assert isinstance(result[0].content[1], MediaPart)
    media = result[0].content[1].media
    assert media.url == 'https://example.com/cat.jpg'
    assert media.content_type == 'image/jpeg'
