import json
import re
from importlib.resources import files
from pathlib import Path

import yaml
from ipywidgets import HTML, Button, HBox, VBox
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers.python import PythonLexer
from pytest import fixture

from scarlet2.questionnaire.models import QuestionAnswer, QuestionAnswers, Questionnaire
from scarlet2.questionnaire.questionnaire import (
    OUTPUT_BOX_LAYOUT,
    OUTPUT_BOX_STYLE_FILE,
    QUESTION_BOX_LAYOUT,
    VIEWS_PACKAGE_PATH,
)


@fixture
def data_dir():
    """Path to the data directory containing the example questionnaire YAML file."""
    return Path(__file__).parent / "data"


@fixture
def example_questionnaire_dict(data_dir):
    """An example questionnaire dictionary"""
    yaml_path = data_dir / "example_questionnaire.yaml"
    with yaml_path.open("r") as f:
        return yaml.safe_load(f)


@fixture
def example_questionnaire(example_questionnaire_dict):
    """An example Questionnaire model instance"""
    return Questionnaire.model_validate(example_questionnaire_dict)


@fixture
def example_questionnaire_with_switch_dict(data_dir):
    """An example questionnaire dictionary with a switch question"""
    yaml_path = data_dir / "example_questionnaire_switch.yaml"
    with yaml_path.open("r") as f:
        return yaml.safe_load(f)


@fixture
def example_questionnaire_with_switch(example_questionnaire_with_switch_dict):
    """An example Questionnaire model instance with a switch question"""
    return Questionnaire.model_validate(example_questionnaire_with_switch_dict)


@fixture
def questionnaire_with_followup_switch_example_dict(data_dir):
    """An example questionnaire dictionary with a switch question"""
    yaml_path = data_dir / "example_questionnaire_followup_switch.yaml"
    with yaml_path.open("r") as f:
        return yaml.safe_load(f)


@fixture
def example_questionnaire_with_followup_switch(questionnaire_with_followup_switch_example_dict):
    """An example Questionnaire model instance with a switch question"""
    return Questionnaire.model_validate(questionnaire_with_followup_switch_example_dict)


@fixture
def example_questionnaire_with_feedback(example_questionnaire):
    """An example Questionnaire model instance with a feedback URL"""
    questionnaire = example_questionnaire.model_copy(deep=True)
    questionnaire.feedback_url = "https://example.com/feedback"
    return questionnaire


@fixture
def example_question_answers(example_questionnaire):
    """An example list of question answers for the exmaple questionnaire"""
    answer_inds = [0, 1, 0]  # indices of answers to select for each question
    questions = [
        example_questionnaire.questions[0],
        example_questionnaire.questions[0].answers[0].followups[0],
        example_questionnaire.questions[0].answers[0].followups[1],
    ]
    qas = [
        QuestionAnswer(question=q.question, answer=q.answers[i].answer, value=i)
        for q, i in zip(questions, answer_inds, strict=False)
    ]
    return QuestionAnswers(answers=qas)


class Helpers:
    """Helper functions for testing the QuestionnaireWidget"""

    @staticmethod
    def get_answer_button(widget, answer_index):
        """Get an answer button from the question box children.

        Args:
            widget: The QuestionnaireWidget instance
            answer_index: The index of the answer button to get

        Returns:
            The answer button widget
        """
        css_offset = 1
        prev_questions_offset = len(widget.question_answers)
        question_label_offset = 1

        # Get the buttons container which is at index after the question label
        buttons_container_index = css_offset + prev_questions_offset + question_label_offset
        buttons_container = widget.question_box.children[buttons_container_index]

        # Return the specific button from the buttons container
        return buttons_container.children[answer_index]

    @staticmethod
    def get_prev_question_button(widget, question_index):
        """Get a previous question button from the question box children.

        Args:
            widget: The QuestionnaireWidget instance
            question_index: The index of the previous question to get

        Returns:
            The previous question button widget
        """
        css_offset = 1

        container = widget.question_box.children[css_offset + question_index]
        # The button is the first child of the container
        return container.children[0]

    @staticmethod
    def assert_widget_ui_matches_state(widget):
        """Assert that the widget's UI matches its internal state."""
        assert isinstance(widget.ui, HBox)
        assert widget.ui.children == (widget.question_box, widget.output_box)

        assert isinstance(widget.output_box, VBox)
        assert widget.output_box.children == (widget.output_container,)
        assert widget.output_box.layout == OUTPUT_BOX_LAYOUT

        assert isinstance(widget.output_container, HTML)

        # check output container contains css from output_box css file
        css_file = files(VIEWS_PACKAGE_PATH).joinpath(OUTPUT_BOX_STYLE_FILE)
        with css_file.open("r") as f:
            css_content = f.read()

        assert css_content in widget.output_container.value

        output_code = re.sub(r"\{\{\s*\w+\s*\}\}", "", widget.code_output)

        html = widget.output_container.value

        # regex to capture the JS argument
        match = re.search(r"navigator\.clipboard\.writeText\((.*?)\)", html)
        assert match, "No copy button found"

        actual_arg = match.group(1)
        expected_arg = json.dumps(output_code)  # exactly how Jinja|tojson would encode it

        assert actual_arg.strip() == expected_arg

        formatter = HtmlFormatter(style="monokai", noclasses=True)
        highlighted_code = highlight(output_code, PythonLexer(), formatter)

        assert highlighted_code in widget.output_container.value

        assert isinstance(widget.question_box, VBox)
        assert widget.question_box.layout == QUESTION_BOX_LAYOUT

        css_snippet_count = 1
        save_button_container_count = 1

        # If there's a current question, we have:
        # - CSS snippet
        # - Previous question containers
        # - Current question label
        # - Buttons container
        # - Save button container
        if widget.current_question:
            expected_children_count = (
                css_snippet_count + len(widget.question_answers) + 1 + 1 + save_button_container_count
            )
        # If there's no current question, we have:
        # - CSS snippet
        # - Previous question containers
        # - Final message container
        # - Save button container
        else:
            expected_children_count = (
                css_snippet_count + len(widget.question_answers) + 1 + save_button_container_count
            )

        assert len(widget.question_box.children) == expected_children_count

        # Skip the CSS snippet
        css_offset = 1

        for i in range(len(widget.question_answers)):
            # Add the CSS offset to the index
            child_index = i + css_offset
            assert isinstance(widget.question_box.children[child_index], HBox)
            # The button is the first child of the container
            btn = widget.question_box.children[child_index].children[0]
            question = widget.question_answers[i][0]
            assert question.question in btn.description
            ans_index = widget.question_answers[i][1]
            assert question.answers[ans_index].answer in btn.description

        if widget.current_question is not None:
            # Add the CSS offset to the index
            qs_ind = len(widget.question_answers) + css_offset

            assert isinstance(widget.question_box.children[qs_ind], HTML)
            assert widget.current_question.question in widget.question_box.children[qs_ind].value

            # Get the buttons container which is at index after the question label
            buttons_container_index = qs_ind + 1
            buttons_container = widget.question_box.children[buttons_container_index]
            assert isinstance(buttons_container, VBox)

            # Check each button in the buttons container
            for btn, ans in zip(buttons_container.children, widget.current_question.answers, strict=False):
                assert isinstance(btn, Button)
                assert btn.description == ans.answer
                assert btn.tooltip == ans.tooltip

        else:
            # When there's no current question, we have:
            # - CSS snippet
            # - Previous question containers
            # - Final message container
            # - Save button container

            # Get the final message container which is at index before the save button container
            final_message_index = len(widget.question_box.children) - 2
            final_message_container = widget.question_box.children[final_message_index]
            assert isinstance(final_message_container, VBox)

            # The final message is in the HTML child of the container
            final_message_html = final_message_container.children[0]
            assert isinstance(final_message_html, HTML)
            final_message = final_message_html.value
            assert "You're done" in final_message

            # Check for feedback URL if present in the questionnaire
            if widget.feedback_url:
                assert widget.feedback_url in final_message
                assert "feedback form" in final_message

        # Check that the last child is the save button container
        save_button_container = widget.question_box.children[-1]
        assert isinstance(save_button_container, VBox)
        assert len(save_button_container.children) > 0
        save_button = save_button_container.children[-1]
        assert isinstance(save_button, Button)
        assert save_button.description == "Save Answers"


@fixture
def helpers():
    """Provide helper functions for testing."""
    return Helpers()
