"""Integration tests for template preprocessing and variable detection working together.

These tests verify the complete pipeline: preprocess_template converts custom syntax (@, $, @?)
to Jinja2 syntax, then detect_jinja2_required_variables extracts the required variables.
"""

from typing import ClassVar

import pytest

from pipelex.cogt.templating.template_category import TemplateCategory
from pipelex.cogt.templating.template_errors import TemplateSigilSyntaxError
from pipelex.cogt.templating.template_preprocessor import preprocess_template
from pipelex.tools.jinja2.jinja2_required_variables import detect_jinja2_required_variables


class TestData:
    """Test data for template pipeline integration tests."""

    # (topic, raw_template, expected_jinja2, expected_variables)
    # @-sigils must be alone on their line (strict rule). Inline shapes are covered by
    # AT_VARIABLE_PIPELINE_ERRORS below.
    AT_VARIABLE_PIPELINE: ClassVar[list[tuple[str, str, str, set[str]]]] = [
        (
            "single_at_variable_alone",
            "@name",
            '{{ name|tag("name") }}',
            {"name"},
        ),
        (
            "multiple_at_variables_on_lines",
            "@first\n@second",
            '{{ first|tag("first") }}\n{{ second|tag("second") }}',
            {"first", "second"},
        ),
        (
            "at_variable_alone_with_surrounding_lines",
            "Header\n@amount\nFooter",
            'Header\n{{ amount|tag("amount") }}\nFooter',
            {"amount"},
        ),
        (
            "nested_at_variable_alone",
            "@user.profile.name",
            '{{ user.profile.name|tag("user.profile.name") }}',
            {"user.profile.name"},
        ),
    ]

    # Inline @-sigils that violate the strict rule when the root identifier is a declared
    # input of the surrounding pipe. (topic, raw_template, root_identifier)
    AT_VARIABLE_PIPELINE_ERRORS: ClassVar[list[tuple[str, str, str]]] = [
        ("single_at_variable_inline", "Hello @name", "name"),
        ("multiple_at_variables_inline", "@first and @second", "first"),
        ("at_variable_with_trailing_dot_inline", "The value is @amount.", "amount"),
        ("nested_at_variable_inline", "User: @user.profile.name", "user"),
    ]

    DOLLAR_VARIABLE_PIPELINE: ClassVar[list[tuple[str, str, str, set[str]]]] = [
        (
            "single_dollar_variable",
            "Topic: $topic",
            "Topic: {{ topic|format() }}",
            {"topic"},
        ),
        (
            "multiple_dollar_variables",
            "$first and $second",
            "{{ first|format() }} and {{ second|format() }}",
            {"first", "second"},
        ),
        (
            "dollar_variable_with_trailing_dot",
            "The price is $price.",
            "The price is {{ price|format() }}.",
            {"price"},
        ),
        (
            "nested_dollar_variable",
            "Config: $config.setting.value",
            "Config: {{ config.setting.value|format() }}",
            {"config.setting.value"},
        ),
    ]

    # @? sigils must be alone on their line (strict rule).
    OPTIONAL_VARIABLE_PIPELINE: ClassVar[list[tuple[str, str, str, set[str]]]] = [
        (
            "single_optional_variable_alone",
            "@?optional_field",
            '{% if optional_field %}{{ optional_field|tag("optional_field") }}{% endif %}',
            {"optional_field"},
        ),
        (
            "optional_with_surrounding_lines",
            "Notes:\n@?notes\nDone.",
            'Notes:\n{% if notes %}{{ notes|tag("notes") }}{% endif %}\nDone.',
            {"notes"},
        ),
        (
            "nested_optional_variable",
            "@?user.profile.bio",
            '{% if user.profile.bio %}{{ user.profile.bio|tag("user.profile.bio") }}{% endif %}',
            {"user.profile.bio"},
        ),
    ]

    MIXED_PATTERNS_PIPELINE: ClassVar[list[tuple[str, str, str, set[str]]]] = [
        (
            "all_three_patterns",
            """Summary for $name:
@description
@?notes""",
            """Summary for {{ name|format() }}:
{{ description|tag("description") }}
{% if notes %}{{ notes|tag("notes") }}{% endif %}""",
            {"name", "description", "notes"},
        ),
        (
            "complex_mixed_template",
            """Report for $report_title

@main_content

Additional details:
@?extra_info

Generated by: $author_name""",
            """Report for {{ report_title|format() }}

{{ main_content|tag("main_content") }}

Additional details:
{% if extra_info %}{{ extra_info|tag("extra_info") }}{% endif %}

Generated by: {{ author_name|format() }}""",
            {"report_title", "main_content", "extra_info", "author_name"},
        ),
    ]

    # Real-world complex templates — every @-sigil sits alone on its own line.
    # Inline values use $-sigils.
    REAL_WORLD_TEMPLATES: ClassVar[list[tuple[str, str, set[str]]]] = [
        (
            "gantt_chart_analysis",
            """I am sharing an image of a Gantt chart: $gantt_chart_image.
Please analyse the image and for a given task name (and only this task), extract the information of the task, if relevant.

Be careful, the time unit is this:
@gantt_timescale

If the task is a milestone, then only output the start_date.

Here is the name of the task you have to extract the dates for:
@gantt_task_name""",
            {"gantt_chart_image", "gantt_timescale", "gantt_task_name"},
        ),
        (
            "invoice_extraction_prompt",
            """Extract employee information from this invoice text:
@invoice_text

The company name is: $company_name

Please extract the following fields:
- Employee name
- Employee ID
- Department

@?additional_instructions""",
            {"invoice_text", "company_name", "additional_instructions"},
        ),
        (
            "document_analysis",
            """Analyze the following document: $document_content.

Context information:
@context_info

@?reference_documents

Please provide:
1. Summary of key points
2. Action items if any
3. Any risks or concerns

Output format:
@output_format""",
            {"document_content", "context_info", "reference_documents", "output_format"},
        ),
        (
            "image_generation_prompt",
            """Generate an image with the following characteristics:

Subject:
@subject_description
Style: $art_style
Mood:
@mood

@?additional_details

Camera angle: $camera_angle
Lighting:
@lighting_type""",
            {
                "subject_description",
                "art_style",
                "mood",
                "additional_details",
                "camera_angle",
                "lighting_type",
            },
        ),
        (
            "code_review_prompt",
            """Review the following code:
@code_snippet

Programming language: $language
Code purpose:
@purpose

@?coding_standards

Check for:
- Bugs and errors
- Performance issues
- Security vulnerabilities
- Code style

Expected output format: $output_format""",
            {"code_snippet", "language", "purpose", "coding_standards", "output_format"},
        ),
        (
            "multi_section_document",
            """# $document_title

## Introduction
@introduction_text

## Main Content
@main_content

## Optional Sections
@?section_a
@?section_b
@?section_c

## Conclusion
@conclusion_text

---
Author: $author_name
Date: $document_date
Version:
@version_info""",
            {
                "document_title",
                "introduction_text",
                "main_content",
                "section_a",
                "section_b",
                "section_c",
                "conclusion_text",
                "author_name",
                "document_date",
                "version_info",
            },
        ),
    ]

    # Edge cases with numbers
    NUMBER_EDGE_CASES: ClassVar[list[tuple[str, str, str, set[str]]]] = [
        (
            "dollar_amount_not_variable",
            "The price is $10 and $20",
            "The price is $10 and $20",  # Should not be converted
            set(),
        ),
        (
            "at_version_not_variable",
            "Version @1.0 is released",
            "Version @1.0 is released",  # Should not be converted
            set(),
        ),
        (
            "mixed_amounts_and_variables",
            "Price is $10 but budget is $budget_amount",
            "Price is $10 but budget is {{ budget_amount|format() }}",
            {"budget_amount"},
        ),
        (
            "version_and_at_variable",
            "Version @1.0 with $release_notes",
            "Version @1.0 with {{ release_notes|format() }}",
            {"release_notes"},
        ),
    ]


class TestTemplatePipelineIntegration:
    """Tests for the complete template preprocessing and variable detection pipeline."""

    @pytest.mark.parametrize(
        ("topic", "raw_template", "expected_jinja2", "expected_variables"),
        TestData.AT_VARIABLE_PIPELINE,
    )
    def test_at_variable_pipeline(
        self,
        topic: str,
        raw_template: str,
        expected_jinja2: str,
        expected_variables: set[str],
    ):
        """Test @ variable pattern through the complete pipeline."""
        # Step 1: Preprocess
        processed = preprocess_template(raw_template)
        assert processed == expected_jinja2, f"Preprocessing failed for topic: {topic}"

        # Step 2: Detect variables
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == expected_variables, f"Variable detection failed for topic: {topic}"

    @pytest.mark.parametrize(
        ("topic", "raw_template", "root_identifier"),
        TestData.AT_VARIABLE_PIPELINE_ERRORS,
    )
    def test_at_variable_pipeline_raises_when_inline(self, topic: str, raw_template: str, root_identifier: str):
        """Inline @-sigils whose root identifier is a declared input violate the strict
        line-bounded rule and raise. The `root_identifier` from the parametrized case is
        passed as `declared_inputs` to activate the gate.
        """
        with pytest.raises(TemplateSigilSyntaxError) as exc_info:
            preprocess_template(raw_template, declared_inputs={root_identifier})
        assert "line " in str(exc_info.value), f"Missing line number in error for topic: {topic}"

    @pytest.mark.parametrize(
        ("topic", "raw_template", "expected_jinja2", "expected_variables"),
        TestData.DOLLAR_VARIABLE_PIPELINE,
    )
    def test_dollar_variable_pipeline(
        self,
        topic: str,
        raw_template: str,
        expected_jinja2: str,
        expected_variables: set[str],
    ):
        """Test $ variable pattern through the complete pipeline."""
        # Step 1: Preprocess
        processed = preprocess_template(raw_template)
        assert processed == expected_jinja2, f"Preprocessing failed for topic: {topic}"

        # Step 2: Detect variables
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == expected_variables, f"Variable detection failed for topic: {topic}"

    @pytest.mark.parametrize(
        ("topic", "raw_template", "expected_jinja2", "expected_variables"),
        TestData.OPTIONAL_VARIABLE_PIPELINE,
    )
    def test_optional_variable_pipeline(
        self,
        topic: str,
        raw_template: str,
        expected_jinja2: str,
        expected_variables: set[str],
    ):
        """Test @? optional variable pattern through the complete pipeline."""
        # Step 1: Preprocess
        processed = preprocess_template(raw_template)
        assert processed == expected_jinja2, f"Preprocessing failed for topic: {topic}"

        # Step 2: Detect variables
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == expected_variables, f"Variable detection failed for topic: {topic}"

    @pytest.mark.parametrize(
        ("topic", "raw_template", "expected_jinja2", "expected_variables"),
        TestData.MIXED_PATTERNS_PIPELINE,
    )
    def test_mixed_patterns_pipeline(
        self,
        topic: str,
        raw_template: str,
        expected_jinja2: str,
        expected_variables: set[str],
    ):
        """Test templates with mixed @, $, and @? patterns."""
        # Step 1: Preprocess
        processed = preprocess_template(raw_template)
        assert processed == expected_jinja2, f"Preprocessing failed for topic: {topic}"

        # Step 2: Detect variables
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == expected_variables, f"Variable detection failed for topic: {topic}"

    @pytest.mark.parametrize(
        ("topic", "raw_template", "expected_variables"),
        TestData.REAL_WORLD_TEMPLATES,
    )
    def test_real_world_templates(
        self,
        topic: str,
        raw_template: str,
        expected_variables: set[str],
    ):
        """Test real-world complex templates through the pipeline."""
        # Step 1: Preprocess
        processed = preprocess_template(raw_template)

        # Step 2: Detect variables
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == expected_variables, f"Variable detection failed for topic: {topic}"

    @pytest.mark.parametrize(
        ("topic", "raw_template", "expected_jinja2", "expected_variables"),
        TestData.NUMBER_EDGE_CASES,
    )
    def test_number_edge_cases(
        self,
        topic: str,
        raw_template: str,
        expected_jinja2: str,
        expected_variables: set[str],
    ):
        """Test edge cases where $ and @ are followed by numbers (not variables)."""
        # Step 1: Preprocess
        processed = preprocess_template(raw_template)
        assert processed == expected_jinja2, f"Preprocessing failed for topic: {topic}"

        # Step 2: Detect variables
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == expected_variables, f"Variable detection failed for topic: {topic}"

    def test_no_variables_template(self):
        """Test a template with no special syntax passes through unchanged."""
        raw_template = "This is a plain text template with no variables."

        processed = preprocess_template(raw_template)
        assert processed == raw_template

        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == set()

    def test_already_jinja2_template(self):
        """Test a template that already has Jinja2 syntax."""
        raw_template = "Hello {{ name }}, welcome to {{ place }}"

        processed = preprocess_template(raw_template)
        assert processed == raw_template  # Should not be modified

        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == {"name", "place"}

    def test_mixed_jinja2_and_custom_syntax(self):
        """Test a template with both Jinja2 and custom syntax."""
        raw_template = """Hello {{ existing_var }}!
Your topic is: $topic
Additional info:
@info
Optional:
@?optional_data"""

        processed = preprocess_template(raw_template)
        expected = """Hello {{ existing_var }}!
Your topic is: {{ topic|format() }}
Additional info:
{{ info|tag("info") }}
Optional:
{% if optional_data %}{{ optional_data|tag("optional_data") }}{% endif %}"""
        assert processed == expected

        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        assert variables == {"existing_var", "topic", "info", "optional_data"}

    @pytest.mark.parametrize(
        "template_category",
        [
            TemplateCategory.BASIC,
            TemplateCategory.LLM_PROMPT,
            TemplateCategory.HTML,
            TemplateCategory.MARKDOWN,
            TemplateCategory.IMG_GEN_PROMPT,
        ],
    )
    def test_pipeline_across_template_categories(self, template_category: TemplateCategory):
        """Test the pipeline works across different template categories."""
        raw_template = """Title: $title
Content:
@content
Optional:
@?optional"""

        processed = preprocess_template(raw_template)
        expected = """Title: {{ title|format() }}
Content:
{{ content|tag("content") }}
Optional:
{% if optional %}{{ optional|tag("optional") }}{% endif %}"""
        assert processed == expected

        variables = detect_jinja2_required_variables(
            template_category=template_category,
            template_source=processed,
        )
        assert variables == {"title", "content", "optional"}

    def test_multiline_template_with_all_patterns(self):
        """Test a complex multiline template with all pattern types."""
        raw_template = """
==============================
Report: $report_name
==============================

Section 1: Overview
-------------------
@overview_content

Section 2: Details
------------------
@details_content

Section 3: Optional Notes
-------------------------
@?optional_notes

Section 4: Summary
------------------
@summary_content

==============================
Generated for: $recipient_name
Date: $generation_date
==============================
"""

        processed = preprocess_template(raw_template)

        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )

        expected_vars = {
            "report_name",
            "overview_content",
            "details_content",
            "optional_notes",
            "summary_content",
            "recipient_name",
            "generation_date",
        }
        assert variables == expected_vars

    def test_variable_used_multiple_times(self):
        """Test that a variable used multiple times is detected only once.

        Under the strict rule, the `@`/`@?` forms must each sit alone on their line; the
        inline shape is the `$` form.
        """
        raw_template = """
@name
And here again:
@name
Also $name as dollar variable.
And as optional:
@?name
"""
        processed = preprocess_template(raw_template)
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        # All should resolve to the same variable
        assert variables == {"name"}

    def test_deeply_nested_variable_names(self):
        """Test variables with deep dot notation. @-sigils sit alone on their line."""
        raw_template = """
User info:
@user.profile.settings.preferences.theme
Config: $app.config.database.connection.host
Optional:
@?system.logs.archive.old.entries
"""
        processed = preprocess_template(raw_template)
        variables = detect_jinja2_required_variables(
            template_category=TemplateCategory.LLM_PROMPT,
            template_source=processed,
        )
        # Full variable paths are detected
        assert variables == {
            "user.profile.settings.preferences.theme",
            "app.config.database.connection.host",
            "system.logs.archive.old.entries",
        }
