"""Tests for issue #484 — collapsible JSON/YAML tree viewer.

The original feature hardcoded the default view: valid JSON/YAML fenced blocks
opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`). That default
is now user-configurable (#484 follow-up) via two settings:

  - structured_code_default_view: "auto" | "on" | "off"
        on   => always default to Tree
        off  => always default to Raw
        auto => Tree only when the block line count >= the configured threshold
  - structured_code_auto_tree_lines: integer 1..1000 (default 10)

`auto` + threshold 10 reproduces the original behavior, so the default is
preserved. These tests pin the new configurable shape while keeping the existing
Tree/Raw renderer invariants (wrapper class, helpers, value types, toggle, YAML
support) intact.
"""
import shutil
import subprocess
from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).parent.parent.resolve()
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
NODE = shutil.which("node")


class TestTreeRenderer:
    """Fenced JSON/YAML blocks should get a tree view toggle."""

    def test_json_blocks_get_tree_wrapper(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "code-tree-wrap" in content
        assert "data-raw" in content
        assert "data-lang" in content

    def test_json_yaml_lang_detection(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "lang==='json'||lang==='yaml'" in content

    def test_initTreeViews_function_exists(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "function initTreeViews" in content

    def test_buildTreeDOM_function_exists(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "function _buildTreeDOM(val, depth)" in content

    def test_initTreeViews_called_in_post_render(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        # Behavior assertion (#5338): post-render scheduled via
        # _postProcessWithAnchorSuppression, which still calls postProcessRenderedMessages.
        assert "requestAnimationFrame(()=>_postProcessWithAnchorSuppression(" in content
        _wrap = content.find('function _postProcessWithAnchorSuppression')
        assert _wrap != -1, "post-render should be wrapped by _postProcessWithAnchorSuppression"
        assert 'postProcessRenderedMessages(container)' in content[_wrap:_wrap + 500], \
            "the wrapper must still invoke postProcessRenderedMessages"
        start = content.find("function postProcessRenderedMessages")
        body = content[start:start + 500]
        assert "initTreeViews(container)" in body

    def test_tree_handles_all_value_types(self):
        """_buildTreeDOM should handle null, boolean, number, string, array, object."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        for cls in ("tree-null", "tree-bool", "tree-num", "tree-str", "tree-array", "tree-object"):
            assert cls in content, f"Missing type class: {cls}"

    def test_tree_collapse_support(self):
        """Tree nodes should be collapsible with collapsed/expanded states."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "tree-collapsed" in content
        assert "tree-collapsible" in content
        assert "classList.toggle" in content

    def test_tree_depth_auto_collapse(self):
        """Nested levels beyond depth 2 should be collapsed by default."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "depth>=2" in content

    def test_toggle_button_uses_i18n(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "t('raw_view')" in content
        assert "t('tree_view')" in content

    def test_yaml_support_via_jsyaml(self):
        """YAML should be parsed via jsyaml if available."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "jsyaml" in content


class TestConfigurableDefaultView:
    """The default Tree/Raw decision is configurable, not hardcoded (#484 follow-up)."""

    def test_hardcoded_threshold_is_gone(self):
        """The original `lineCount>=10` hardcode must no longer drive the default."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "const showTree=lineCount>=10;" not in content, (
            "The hardcoded `const showTree=lineCount>=10;` must be replaced by the "
            "configurable decision helper."
        )
        assert "lineCount>=10" not in content, (
            "No hardcoded `lineCount>=10` comparison should remain; the threshold "
            "is configurable and the default is read from a sanitized helper."
        )

    def test_decision_helper_exists(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "function _structuredCodeShowTree(mode,threshold,lineCount)" in content
        assert "function _structuredCodeMode(" in content
        assert "function _structuredCodeThreshold(" in content

    def test_all_three_modes_represented(self):
        """auto / on / off must all be handled in the renderer."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        start = content.find("function _structuredCodeShowTree")
        body = content[start:start + 400]
        assert "'on'" in body, "mode 'on' (always Tree) must be handled"
        assert "'off'" in body, "mode 'off' (always Raw) must be handled"
        # 'auto' is the fallthrough; the mode reader sanitizes to it.
        mode_start = content.find("function _structuredCodeMode")
        mode_body = content[mode_start:mode_start + 300]
        assert "'auto'" in mode_body, "mode 'auto' must be the sanitized fallback"

    def test_default_threshold_fallback_is_10(self):
        """An invalid/missing threshold must fall back to 10 (original behavior)."""
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        thr_start = content.find("function _structuredCodeThreshold")
        thr_body = content[thr_start:thr_start + 300]
        assert ":10" in thr_body or "?10" in thr_body or " 10" in thr_body, (
            "_structuredCodeThreshold must fall back to 10 for invalid/missing values"
        )
        # Sanitization bounds (1..1000) keep the value safe.
        assert "1000" in thr_body, "threshold should be clamped to an upper bound"

    def test_renderer_uses_helper_for_decision(self):
        with open("static/ui.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "_structuredCodeShowTree(_structuredCodeMode(),_structuredCodeThreshold(),lineCount)" in content

    @pytest.mark.skipif(NODE is None, reason="node not on PATH")
    def test_decision_helper_behaviour_via_node(self, tmp_path):
        """Evaluate the real _structuredCodeShowTree() against the prompt's cases.

          - off, lineCount 999            => False
          - on, lineCount 1               => True
          - auto, threshold 10, count 9   => False
          - auto, threshold 10, count 10  => True
          - auto, invalid threshold       => fallback 10 (count 10 => True, 9 => False)
        """
        src = UI_JS_PATH.read_text(encoding="utf-8")
        # Extract the brace-balanced body of _structuredCodeShowTree.
        marker = "function _structuredCodeShowTree("
        start = src.find(marker)
        assert start >= 0, "_structuredCodeShowTree not found"
        i = src.index("{", start)
        depth = 1
        i += 1
        while depth > 0 and i < len(src):
            if src[i] == "{":
                depth += 1
            elif src[i] == "}":
                depth -= 1
            i += 1
        fn_src = src[start:i]

        driver = tmp_path / "driver.js"
        driver.write_text(
            fn_src
            + "\n"
            + r"""
const cases = [
  ['off', 5, 999, false],
  ['on', 5, 1, true],
  ['auto', 10, 9, false],
  ['auto', 10, 10, true],
  ['auto', 11, 10, false],
  // invalid threshold falls back to 10
  ['auto', NaN, 10, true],
  ['auto', NaN, 9, false],
  // out-of-range thresholds fall back to 10 inside the pure helper
  ['auto', 0, 10, true],
  ['auto', 0, 9, false],
  ['auto', 99999, 10, true],
  ['auto', 99999, 9, false],
  // unknown mode is treated as auto by the pure helper's fallthrough
  ['bogus', 10, 10, true],
  ['bogus', 10, 9, false],
];
let failures = [];
for (const [mode, thr, count, want] of cases) {
  const got = _structuredCodeShowTree(mode, thr, count);
  if (got !== want) failures.push(`${mode},${thr},${count} => ${got} (want ${want})`);
}
if (failures.length) { console.error('FAIL: ' + failures.join(' | ')); process.exit(1); }
console.log('OK');
""",
            encoding="utf-8",
        )
        result = subprocess.run(
            [NODE, str(driver)], capture_output=True, text=True, timeout=30
        )
        assert result.returncode == 0, (
            f"_structuredCodeShowTree behaviour mismatch: {result.stdout}{result.stderr}"
        )


class TestTreeCSS:
    """CSS classes for tree viewer."""

    def test_tree_css_classes_exist(self):
        with open("static/style.css", "r", encoding="utf-8") as f:
            content = f.read()
        for cls in (".code-tree-wrap", ".tree-view", ".tree-hidden", ".tree-toggle-btn",
                    ".tree-node", ".tree-collapsible", ".tree-children", ".tree-collapsed",
                    ".tree-key", ".tree-str", ".tree-num", ".tree-bool", ".tree-null",
                    ".tree-comma", ".tree-item"):
            assert cls in content, f"Missing CSS: {cls}"

    def test_tree_colors_match_types(self):
        with open("static/style.css", "r", encoding="utf-8") as f:
            content = f.read()
        # Green strings, blue numbers, amber booleans
        assert "#4ade80" in content  # tree-str green
        assert "#60a5fa" in content  # tree-key/tree-num blue
        assert "#fbbf24" in content  # tree-bool amber


class TestTreeI18n:
    def test_i18n_keys_present(self):
        with open("static/i18n.js", "r", encoding="utf-8") as f:
            content = f.read()
        for key in ("tree_view", "raw_view"):
            count = content.count(key)
            assert count >= 7, f"{key} found {count} times, expected >= 7"

    def test_structured_code_setting_i18n_keys_present(self):
        """The new settings labels/options/help text must exist in i18n."""
        with open("static/i18n.js", "r", encoding="utf-8") as f:
            content = f.read()
        for key in (
            "settings_label_structured_code",
            "settings_option_structured_code_auto",
            "settings_option_structured_code_on",
            "settings_option_structured_code_off",
            "settings_label_structured_code_auto_lines",
            "settings_desc_structured_code",
        ):
            assert f"{key}:" in content, f"Missing i18n key: {key}"


class TestStructuredCodeSettingsWiring:
    """The setting must be a real, persisted WebUI setting — not a localStorage hack."""

    def test_server_defaults_and_validation(self):
        with open("api/config.py", "r", encoding="utf-8") as f:
            content = f.read()
        # Defaults preserve current behavior: auto + threshold 10.
        assert '"structured_code_default_view": "auto"' in content
        assert '"structured_code_auto_tree_lines": 10' in content
        # Enum + range validation are registered.
        assert '"structured_code_default_view": {"auto", "on", "off"}' in content
        assert '"structured_code_auto_tree_lines": (1, 1000)' in content

    def test_settings_ui_controls_present(self):
        with open("static/index.html", "r", encoding="utf-8") as f:
            content = f.read()
        assert 'id="settingsStructuredCodeMode"' in content
        assert 'id="settingsStructuredCodeAutoLines"' in content

    def test_boot_initializes_runtime_globals(self):
        with open("static/boot.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "window._structuredCodeDefaultView" in content
        assert "window._structuredCodeAutoTreeLines" in content

    def test_panel_persists_setting(self):
        with open("static/panels.js", "r", encoding="utf-8") as f:
            content = f.read()
        assert "_structuredCodeViewFromUi" in content
        assert "structured_code_default_view" in content
        assert "structured_code_auto_tree_lines" in content
