"""Unit tests for AI fix service helper functions.

Tests pure Python helpers only — no AI mocking needed.
"""

import pytest

from app.core.ai_fix_service import (
    _find_node_by_id,
    _find_parent_node,
    _serialize_tree_outline,
    _strip_markdown_fences,
    _replace_node_in_tree,
    _describe_fix,
)


# ── Sample tree ──

SAMPLE_TREE = {
    "id": "root",
    "type": "decision",
    "question": "Is the server up?",
    "options": [
        {"id": "opt-yes", "label": "Yes", "next_node_id": "check-logs"},
        {"id": "opt-no", "label": "No", "next_node_id": "restart"},
    ],
    "children": [
        {
            "id": "check-logs",
            "type": "action",
            "title": "Check Logs",
            "description": "Review logs.",
            "next_node_id": "logs-ok",
        },
        {
            "id": "logs-ok",
            "type": "solution",
            "title": "Logs OK",
            "description": "Issue in logs.",
        },
        {
            "id": "restart",
            "type": "decision",
            "question": "Did restart work?",
            "options": [{"id": "opt-r", "label": "Yes", "next_node_id": "done"}],
            "children": [
                {
                    "id": "done",
                    "type": "solution",
                    "title": "Done",
                    "description": "Fixed.",
                }
            ],
        },
    ],
}


# ── _find_node_by_id ──


class TestFindNodeById:
    def test_finds_root(self):
        node = _find_node_by_id(SAMPLE_TREE, "root")
        assert node is not None
        assert node["id"] == "root"
        assert node["type"] == "decision"

    def test_finds_nested_child(self):
        node = _find_node_by_id(SAMPLE_TREE, "done")
        assert node is not None
        assert node["id"] == "done"
        assert node["type"] == "solution"

    def test_finds_direct_child(self):
        node = _find_node_by_id(SAMPLE_TREE, "check-logs")
        assert node is not None
        assert node["title"] == "Check Logs"

    def test_returns_none_for_missing(self):
        node = _find_node_by_id(SAMPLE_TREE, "nonexistent")
        assert node is None

    def test_returns_none_for_non_dict(self):
        assert _find_node_by_id("not a dict", "root") is None


# ── _find_parent_node ──


class TestFindParentNode:
    def test_root_has_no_parent(self):
        parent = _find_parent_node(SAMPLE_TREE, "root")
        assert parent is None

    def test_finds_parent_of_direct_child(self):
        parent = _find_parent_node(SAMPLE_TREE, "check-logs")
        assert parent is not None
        assert parent["id"] == "root"

    def test_finds_parent_of_deeply_nested(self):
        parent = _find_parent_node(SAMPLE_TREE, "done")
        assert parent is not None
        assert parent["id"] == "restart"

    def test_returns_none_for_missing(self):
        parent = _find_parent_node(SAMPLE_TREE, "nonexistent")
        assert parent is None

    def test_returns_none_for_non_dict(self):
        assert _find_parent_node("not a dict", "root") is None


# ── _serialize_tree_outline ──


class TestSerializeTreeOutline:
    def test_produces_readable_outline(self):
        outline = _serialize_tree_outline(SAMPLE_TREE)
        assert "- [decision] Is the server up?" in outline
        assert "  - [action] Check Logs" in outline
        assert "  - [solution] Logs OK" in outline
        assert "    - [solution] Done" in outline

    def test_marks_error_node(self):
        outline = _serialize_tree_outline(SAMPLE_TREE, error_node_id="restart")
        assert "<<< ERROR HERE" in outline
        # Only the restart node should be marked
        lines = outline.split("\n")
        error_lines = [l for l in lines if "ERROR HERE" in l]
        assert len(error_lines) == 1
        assert "Did restart work?" in error_lines[0]

    def test_no_error_marker_when_none(self):
        outline = _serialize_tree_outline(SAMPLE_TREE)
        assert "ERROR HERE" not in outline

    def test_handles_non_dict(self):
        assert _serialize_tree_outline("not a dict") == ""

    def test_indentation_increases_with_depth(self):
        outline = _serialize_tree_outline(SAMPLE_TREE)
        lines = outline.split("\n")
        # Root has no indentation
        assert lines[0].startswith("- [decision]")
        # Children have 2-space indent
        child_lines = [l for l in lines if "Check Logs" in l]
        assert child_lines[0].startswith("  - ")


# ── _strip_markdown_fences ──


class TestStripMarkdownFences:
    def test_strips_json_fences(self):
        text = '```json\n{"key": "value"}\n```'
        assert _strip_markdown_fences(text) == '{"key": "value"}'

    def test_strips_plain_fences(self):
        text = '```\n{"key": "value"}\n```'
        assert _strip_markdown_fences(text) == '{"key": "value"}'

    def test_passes_through_plain_json(self):
        text = '{"key": "value"}'
        assert _strip_markdown_fences(text) == '{"key": "value"}'


# ── _replace_node_in_tree ──


class TestReplaceNodeInTree:
    def test_replaces_root(self):
        import copy

        tree = copy.deepcopy(SAMPLE_TREE)
        replacement = {"id": "root", "type": "decision", "question": "New question"}
        assert _replace_node_in_tree(tree, "root", replacement) is True
        assert tree["question"] == "New question"
        assert "children" not in tree  # cleared and replaced

    def test_replaces_nested_node(self):
        import copy

        tree = copy.deepcopy(SAMPLE_TREE)
        replacement = {"id": "done", "type": "solution", "title": "All Done", "description": "Complete."}
        assert _replace_node_in_tree(tree, "done", replacement) is True
        found = _find_node_by_id(tree, "done")
        assert found["title"] == "All Done"

    def test_returns_false_for_missing(self):
        import copy

        tree = copy.deepcopy(SAMPLE_TREE)
        assert _replace_node_in_tree(tree, "nonexistent", {"id": "x"}) is False


# ── _describe_fix ──


class TestDescribeFix:
    def test_describes_added_children(self):
        original = {"id": "n1", "children": [{"id": "c1"}]}
        fixed = {"id": "n1", "children": [{"id": "c1"}, {"id": "c2"}]}
        desc = _describe_fix(original, fixed)
        assert "1 child node" in desc

    def test_describes_added_options(self):
        original = {"id": "n1", "options": [{"id": "o1"}]}
        fixed = {"id": "n1", "options": [{"id": "o1"}, {"id": "o2"}]}
        desc = _describe_fix(original, fixed)
        assert "1 option" in desc

    def test_describes_added_next_node_id(self):
        original = {"id": "n1", "type": "action"}
        fixed = {"id": "n1", "type": "action", "next_node_id": "n2"}
        desc = _describe_fix(original, fixed)
        assert "next_node_id" in desc

    def test_fallback_description(self):
        original = {"id": "n1", "type": "solution"}
        fixed = {"id": "n1", "type": "solution"}
        desc = _describe_fix(original, fixed)
        assert "fixed structural issue" in desc.lower()
