"""End-to-end integration tests – Dotprompt render → OpenAI adapter generate."""

from __future__ import annotations

import json

import jsonschema
import pytest

from dotpromptz.typing import (
    DataArgument,
    Message,
    PromptOutputConfig,
    RenderedPrompt,
    Role,
    TextPart,
    ToolDefinition,
)

pytestmark = pytest.mark.integration


# ---------------------------------------------------------------------------
# Test A – adapter.generate() with richer config
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_adapter_generate_with_temperature(openai_adapter) -> None:
    """Call generate() with explicit temperature / max_tokens."""
    rendered = RenderedPrompt(
        config={'model': 'gpt-4.1', 'temperature': 0.0, 'maxTokens': 30},
        messages=[
            Message(role=Role.SYSTEM, content=[TextPart(text='You are a concise assistant.')]),
            Message(role=Role.USER, content=[TextPart(text='What is 2 + 2?')]),
        ],
    )

    response = await openai_adapter.generate(rendered)

    assert response.text is not None
    assert '4' in response.text
    assert response.model is not None
    assert response.usage is not None
    assert response.usage.prompt_tokens > 0
    assert response.usage.completion_tokens > 0
    print(f'\n[e2e] model={response.model}  text={response.text!r}')


# ---------------------------------------------------------------------------
# Test B – full Dotprompt → render → generate pipeline
# ---------------------------------------------------------------------------

_PROMPT_SOURCE = """\
---
model: gpt-4.1
config:
  temperature: 0
---
{{role "system"}}
You are a geography expert. Answer in one sentence.
{{role "user"}}
What is the capital of {{country}}?
"""


@pytest.mark.asyncio
async def test_dotprompt_render_then_generate(dotprompt_instance, openai_adapter) -> None:
    """Render a .prompt template and send it through the OpenAI adapter."""
    rendered = dotprompt_instance.render(
        _PROMPT_SOURCE,
        data=DataArgument(input={'country': 'France'}),
    )

    # Verify rendering produced the expected messages
    assert len(rendered.messages) >= 2
    assert rendered.messages[0].role == Role.SYSTEM
    assert rendered.messages[-1].role == Role.USER

    # Call the real API
    response = await openai_adapter.generate(rendered)

    assert response.text is not None
    assert 'paris' in response.text.lower()
    print(f'\n[e2e] rendered → response: {response.text!r}')


# ---------------------------------------------------------------------------
# Test C – multi-turn conversation
# ---------------------------------------------------------------------------

_MULTITURN_SOURCE = """\
---
model: gpt-4.1
config:
  temperature: 0
---
{{role "system"}}
You are a math tutor. Be concise.
{{role "user"}}
What is 10 * 5?
{{role "model"}}
50
{{role "user"}}
Now divide that by 2.
"""


@pytest.mark.asyncio
async def test_multiturn_conversation(dotprompt_instance, openai_adapter) -> None:
    """Render a multi-turn prompt and verify the model follows context."""
    rendered = dotprompt_instance.render(
        _MULTITURN_SOURCE,
        data=DataArgument(),
    )

    # Should have system + user + assistant + user = 4 messages
    assert len(rendered.messages) >= 4

    response = await openai_adapter.generate(rendered)

    assert response.text is not None
    assert '25' in response.text
    print(f'\n[e2e] multi-turn response: {response.text!r}')


# ---------------------------------------------------------------------------
# Test D – tool calling (function calling)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_tool_calling(openai_adapter) -> None:
    """Verify the model returns a tool call when tools are provided."""
    rendered = RenderedPrompt(
        config={'model': 'gpt-4.1', 'temperature': 0},
        messages=[
            Message(
                role=Role.SYSTEM,
                content=[TextPart(text='You are a helpful assistant. Use the provided tools when appropriate.')],
            ),
            Message(
                role=Role.USER,
                content=[TextPart(text='What is the weather in Tokyo right now?')],
            ),
        ],
        tool_defs=[
            ToolDefinition(
                name='get_weather',
                description='Get the current weather for a city.',
                input_schema={
                    'type': 'object',
                    'properties': {
                        'city': {'type': 'string', 'description': 'The city name'},
                    },
                    'required': ['city'],
                },
            ),
        ],
    )

    response = await openai_adapter.generate(rendered)

    # The model should request the get_weather tool
    assert len(response.tool_calls) > 0
    tc = response.tool_calls[0]
    assert tc.name == 'get_weather'
    assert 'tokyo' in tc.arguments.get('city', '').lower()
    assert response.finish_reason == 'tool_calls'
    print(f'\n[e2e] tool_call: {tc.name}({tc.arguments})')


# ---------------------------------------------------------------------------
# Test E – structured JSON schema output
# ---------------------------------------------------------------------------

# A strict schema with enum constraint to prove enforcement, not guessing.
_PERSON_SCHEMA = {
    'type': 'object',
    'properties': {
        'name': {'type': 'string'},
        'age': {'type': 'integer'},
        'favorite_color': {'type': 'string', 'enum': ['red', 'green', 'blue']},
    },
    'required': ['name', 'age', 'favorite_color'],
    'additionalProperties': False,
}


@pytest.mark.asyncio
async def test_structured_json_schema_output(openai_adapter) -> None:
    """Verify the model response conforms to a strict JSON schema.

    Uses additionalProperties=false and an enum constraint so that
    schema enforcement (not model luck) guarantees conformance.
    """
    rendered = RenderedPrompt(
        config={'model': 'gpt-4.1', 'temperature': 0},
        messages=[
            Message(
                role=Role.SYSTEM,
                content=[TextPart(text='You are a data extraction assistant. Return ONLY valid JSON.')],
            ),
            Message(
                role=Role.USER,
                content=[
                    TextPart(
                        text=(
                            'Extract person info: Alice is 30 years old and her '
                            'favorite color is blue.'
                        )
                    )
                ],
            ),
        ],
        output=PromptOutputConfig(format='json', schema=_PERSON_SCHEMA),
    )

    response = await openai_adapter.generate(rendered)

    assert response.text is not None
    data = json.loads(response.text)

    # Validate against the JSON schema – this would fail if the provider
    # didn't enforce the schema and the model added extra fields or used
    # wrong types.
    jsonschema.validate(instance=data, schema=_PERSON_SCHEMA)

    # Verify concrete values
    assert data['name'] == 'Alice'
    assert isinstance(data['age'], int)
    assert data['age'] == 30
    assert data['favorite_color'] in ('red', 'green', 'blue')
    assert data['favorite_color'] == 'blue'

    # No extra keys (additionalProperties=false enforced by provider)
    assert set(data.keys()) == {'name', 'age', 'favorite_color'}
    print(f'\n[e2e] structured output: {data}')
