"""Unit tests for build/context.py."""

from __future__ import annotations

import pytest

from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.build.context import (
    assets_context,
    conftest_context,
    directive_context,
    docs_context,
    params_context,
    test_directive_context as _test_directive_ctx,
    test_params_context as _test_params_ctx,
)
from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.build.readers import (
    LibraryInfo,
)
from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.semantics import parse
from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.semantics.ir import (
    DirectiveIR,
)
from tests.unit.conftest import _TRUE_FALSE_SEMANTICS


# -------------------------------------------------------------------
# Shared fixtures
# -------------------------------------------------------------------

@pytest.fixture
def true_false_ir() -> DirectiveIR:
    return parse(
        "H5P.TrueFalse", "H5P.TrueFalse 1.8",
        _TRUE_FALSE_SEMANTICS,
    )


@pytest.fixture
def true_false_lib_info() -> LibraryInfo:
    return LibraryInfo(
        machine_name="H5P.TrueFalse",
        major_version=1,
        minor_version=8,
        runnable=True,
        preloaded_js=("js/h5p-true-false.js",),
        preloaded_css=("css/h5p-true-false.css",),
    )


@pytest.fixture
def font_awesome_lib_info() -> LibraryInfo:
    return LibraryInfo(
        machine_name="FontAwesome",
        major_version=4,
        minor_version=5,
        runnable=False,
        preloaded_js=(),
        preloaded_css=(
            "css/font-awesome.min.css",
            "css/override.css",
        ),
    )


# -------------------------------------------------------------------
# assets_context
# -------------------------------------------------------------------

class TestAssetsContext:

    def test_js_css_paths(
        self, true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = assets_context(
            "h5p-true-false",
            true_false_lib_info,
            "byteb4rb1e.sphinxcontrib.h5p.true_false",
        )
        assert ctx["js_files"] == ["js/h5p-true-false.js"]
        assert ctx["css_files"] == ["css/h5p-true-false.css"]

    def test_vendor_name(
        self, true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = assets_context(
            "h5p-true-false",
            true_false_lib_info,
            "byteb4rb1e.sphinxcontrib.h5p.true_false",
        )
        assert ctx["vendor_name"] == "h5p-true-false"

    def test_import_name(
        self, true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = assets_context(
            "h5p-true-false",
            true_false_lib_info,
            "byteb4rb1e.sphinxcontrib.h5p.true_false",
        )
        assert ctx["import_name"] == \
            "byteb4rb1e.sphinxcontrib.h5p.true_false"

    def test_runnable_flag(
        self, true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = assets_context(
            "h5p-true-false",
            true_false_lib_info,
            "byteb4rb1e.sphinxcontrib.h5p.true_false",
        )
        assert ctx["runnable"] is True

    def test_non_runnable(
        self, font_awesome_lib_info: LibraryInfo,
    ) -> None:
        ctx = assets_context(
            "font-awesome",
            font_awesome_lib_info,
            "byteb4rb1e.sphinxcontrib.h5p.font_awesome",
        )
        assert ctx["runnable"] is False
        assert ctx["js_files"] == []
        assert len(ctx["css_files"]) == 2

    def test_machine_name(
        self, true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = assets_context(
            "h5p-true-false",
            true_false_lib_info,
            "byteb4rb1e.sphinxcontrib.h5p.true_false",
        )
        assert ctx["machine_name"] == "H5P.TrueFalse"


# -------------------------------------------------------------------
# directive_context
# -------------------------------------------------------------------

class TestDirectiveContext:

    def test_node_class(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        assert ctx["node_class"] == "h5p_true_false"

    def test_directive_class(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        assert ctx["directive_class"] == "H5PTrueFalseDirective"

    def test_has_body(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        assert ctx["has_body"] is True
        assert ctx["body_field_name"] == "question"

    def test_boolean_flag_pairs(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        entries = ctx["option_spec"]
        names = [e["name"] for e in entries]
        # Boolean options produce both flag and no- prefix
        assert "enable-retry" in names
        assert "no-enable-retry" in names
        # Check validators
        retry_entry = next(
            e for e in entries if e["name"] == "enable-retry"
        )
        assert retry_entry["validator"] == "directives.flag"
        no_retry = next(
            e for e in entries if e["name"] == "no-enable-retry"
        )
        assert no_retry["validator"] == "directives.flag"

    def test_select_validator(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        entries = ctx["option_spec"]
        correct = next(
            e for e in entries if e["name"] == "correct"
        )
        assert correct["validator"] == "validate_correct"
        # Check validator definition
        validators = ctx["select_validators"]
        assert len(validators) >= 1
        v = next(
            v for v in validators if v["func_name"] == "validate_correct"
        )
        assert v["allowed_values"] == ["true", "false"]

    def test_text_validator(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        entries = ctx["option_spec"]
        feedback = next(
            e for e in entries
            if e["name"] == "feedback-on-correct"
        )
        assert feedback["validator"] == "directives.unchanged"

    def test_runnable_flag(
        self,
        true_false_ir: DirectiveIR,
        true_false_lib_info: LibraryInfo,
    ) -> None:
        ctx = directive_context(true_false_ir, true_false_lib_info)
        assert ctx["runnable"] is True


# -------------------------------------------------------------------
# params_context
# -------------------------------------------------------------------

class TestParamsContext:

    def test_behaviour_field_path(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = params_context(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        mappings = ctx["option_mappings"]
        retry = next(
            m for m in mappings
            if m["option_name"] == "enable-retry"
            and not m["is_negated"]
        )
        assert retry["param_path"] == [
            "behaviour", "enableRetry",
        ]

    def test_negated_boolean_path(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = params_context(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        mappings = ctx["option_mappings"]
        no_retry = next(
            m for m in mappings
            if m["option_name"] == "no-enable-retry"
        )
        assert no_retry["is_negated"] is True
        assert no_retry["param_path"] == [
            "behaviour", "enableRetry",
        ]

    def test_top_level_field_path(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = params_context(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        mappings = ctx["option_mappings"]
        correct = next(
            m for m in mappings
            if m["option_name"] == "correct"
        )
        assert correct["param_path"] == ["correct"]

    def test_body_mapping(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = params_context(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        assert ctx["body_mapping"] is not None
        assert ctx["body_mapping"]["field_name"] == "question"
        assert ctx["body_mapping"]["param_path"] == ["question"]

    def test_all_behaviour_fields_nested(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = params_context(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        mappings = ctx["option_mappings"]
        behaviour_opts = [
            m for m in mappings
            if len(m["param_path"]) >= 2
            and m["param_path"][0] == "behaviour"
            and not m["is_negated"]
        ]
        field_names = {m["field_name"] for m in behaviour_opts}
        assert "enableRetry" in field_names
        assert "enableSolutionsButton" in field_names
        assert "confirmCheckDialog" in field_names
        assert "autoCheck" in field_names
        assert "feedbackOnCorrect" in field_names
        assert "feedbackOnWrong" in field_names


# -------------------------------------------------------------------
# Group prefix reconstruction (QuestionSet-style)
# -------------------------------------------------------------------

INTRO_PAGE_SEMANTICS = [
    {
        "name": "introPage",
        "type": "group",
        "label": "Quiz introduction",
        "fields": [
            {
                "name": "title",
                "type": "text",
                "label": "Title",
            },
            {
                "name": "showIntroPage",
                "type": "boolean",
                "label": "Display introduction",
            },
        ],
    },
    {
        "name": "passPercentage",
        "type": "number",
        "label": "Pass percentage",
        "default": 50,
    },
]


class TestParamsContextGroupPrefix:

    @pytest.fixture
    def ir(self) -> DirectiveIR:
        return parse(
            "H5P.QuestionSet", "H5P.QuestionSet 1.20",
            INTRO_PAGE_SEMANTICS,
        )

    def test_named_group_path(self, ir: DirectiveIR) -> None:
        ctx = params_context(ir, INTRO_PAGE_SEMANTICS)
        mappings = ctx["option_mappings"]
        title = next(
            m for m in mappings
            if m["option_name"] == "intro-page-title"
        )
        assert title["param_path"] == [
            "introPage", "title",
        ]

    def test_top_level_number(self, ir: DirectiveIR) -> None:
        ctx = params_context(ir, INTRO_PAGE_SEMANTICS)
        mappings = ctx["option_mappings"]
        pct = next(
            m for m in mappings
            if m["option_name"] == "pass-percentage"
        )
        assert pct["param_path"] == ["passPercentage"]


# -------------------------------------------------------------------
# test_params_context, test_directive_context, conftest_context,
# docs_context — basic smoke tests
# -------------------------------------------------------------------

class TestTestParamsContext:

    def test_has_test_cases(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = _test_params_ctx(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        assert len(ctx["test_cases"]) > 0
        case = ctx["test_cases"][0]
        assert "test_name" in case
        assert "option_name" in case
        assert "param_path" in case

    def test_snake_name(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = _test_params_ctx(
            true_false_ir, _TRUE_FALSE_SEMANTICS,
        )
        assert ctx["snake_name"] == "h5p_true_false"


class TestTestDirectiveContext:

    def test_directive_class(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = _test_directive_ctx(true_false_ir)
        assert ctx["directive_class"] == "H5PTrueFalseDirective"

    def test_has_body(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = _test_directive_ctx(true_false_ir)
        assert ctx["has_body"] is True

    def test_options_list(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = _test_directive_ctx(true_false_ir)
        assert len(ctx["options"]) > 0
        opt = ctx["options"][0]
        assert "name" in opt
        assert "field_type" in opt
        assert "test_name" in opt


class TestConftestContext:

    def test_import_name(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = conftest_context(
            true_false_ir,
            "byteb4rb1e.sphinxcontrib.h5p.true_false",
        )
        assert ctx["import_name"] == \
            "byteb4rb1e.sphinxcontrib.h5p.true_false"
        assert ctx["directive_name"] == "h5p-true-false"


class TestDocsContext:

    def test_directive_name(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = docs_context(true_false_ir)
        assert ctx["directive_name"] == "h5p-true-false"

    def test_body_info(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = docs_context(true_false_ir)
        assert ctx["has_body"] is True
        assert ctx["body_label"] == "Question"

    def test_options_have_fields(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = docs_context(true_false_ir)
        assert len(ctx["options"]) > 0
        opt = ctx["options"][0]
        assert "name" in opt
        assert "field_type" in opt
        assert "label" in opt

    def test_boolean_option_has_negated(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = docs_context(true_false_ir)
        bool_opts = [
            o for o in ctx["options"]
            if o["field_type"] == "boolean"
        ]
        assert len(bool_opts) > 0
        assert all(
            "negated_name" in o for o in bool_opts
        )

    def test_select_option_has_choices(
        self, true_false_ir: DirectiveIR,
    ) -> None:
        ctx = docs_context(true_false_ir)
        select_opts = [
            o for o in ctx["options"]
            if o["field_type"] == "select"
        ]
        assert len(select_opts) > 0
        assert len(select_opts[0]["choices"]) == 2
