"""
Tests for tree markdown serialization, parsing, and round-trip fidelity.
"""
import pytest

from app.services.tree_markdown_service import serialize_tree_to_markdown
from app.services.tree_markdown_parser import parse_markdown_to_tree
from app.services.tree_markdown_validator import validate_tree_markdown


# --- Fixtures: Tree structures ---

SIMPLE_TREE = {
    "id": "root",
    "type": "decision",
    "question": "Is this a test?",
    "help_text": "Check if this is a test scenario",
    "options": [
        {"id": "yes", "label": "Yes", "next_node_id": "solution1"},
        {"id": "no", "label": "No", "next_node_id": "solution2"},
    ],
    "children": [
        {
            "id": "solution1",
            "type": "solution",
            "title": "Test Confirmed",
            "description": "This is indeed a test.",
            "resolution_steps": ["Step 1", "Step 2"],
            "solution": "Test confirmed",
        },
        {
            "id": "solution2",
            "type": "solution",
            "title": "Not a Test",
            "description": "This is not a test.",
            "solution": "Not a test",
        },
    ],
}

ACTION_TREE = {
    "id": "root",
    "type": "decision",
    "question": "What type of issue?",
    "help_text": "Identify the primary issue",
    "options": [
        {"id": "opt_net", "label": "Network issue", "next_node_id": "check_net"},
    ],
    "children": [
        {
            "id": "check_net",
            "type": "action",
            "title": "Run Network Diagnostics",
            "description": "Check basic connectivity.",
            "commands": ["ping 8.8.8.8", "tracert gateway.local"],
            "expected_outcome": "Ping replies or timeout",
            "next_node_id": "resolved",
            "children": [
                {
                    "id": "resolved",
                    "type": "solution",
                    "title": "Issue Resolved",
                    "description": "Network is working now.",
                    "solution": "Issue resolved",
                }
            ],
        }
    ],
}

DEEPLY_NESTED_TREE = {
    "id": "root",
    "type": "decision",
    "question": "Level 1?",
    "options": [
        {"id": "o1", "label": "Go deeper", "next_node_id": "level2"},
    ],
    "children": [
        {
            "id": "level2",
            "type": "decision",
            "question": "Level 2?",
            "options": [
                {"id": "o2", "label": "Go deeper", "next_node_id": "level3"},
            ],
            "children": [
                {
                    "id": "level3",
                    "type": "decision",
                    "question": "Level 3?",
                    "options": [
                        {"id": "o3", "label": "Done", "next_node_id": "final"},
                    ],
                    "children": [
                        {
                            "id": "final",
                            "type": "solution",
                            "title": "Deep Solution",
                            "description": "Found it at level 3.",
                            "solution": "Deep solution found",
                        }
                    ],
                }
            ],
        }
    ],
}

SINGLE_NODE_TREE = {
    "id": "root",
    "type": "solution",
    "title": "Quick Fix",
    "description": "Just restart the computer.",
    "solution": "Restart the computer",
}


# --- Serialization Tests ---

class TestSerializeTreeToMarkdown:
    def test_simple_tree(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        assert "id: root" in md
        assert "type: decision" in md
        assert "# Is this a test?" in md
        assert "> Check if this is a test scenario" in md
        assert "- [A] Yes → @solution1" in md
        assert "- [B] No → @solution2" in md
        assert "id: solution1" in md
        assert "## Test Confirmed" in md
        assert "1. Step 1" in md
        assert "2. Step 2" in md

    def test_action_tree(self):
        md = serialize_tree_to_markdown(ACTION_TREE)
        assert "## Run Network Diagnostics" in md
        assert "```commands" in md
        assert "ping 8.8.8.8" in md
        assert "**Expected:** Ping replies or timeout" in md
        assert "→ @resolved" in md

    def test_deeply_nested(self):
        md = serialize_tree_to_markdown(DEEPLY_NESTED_TREE)
        assert "id: level2" in md
        assert "parent: root" in md
        assert "id: level3" in md
        assert "parent: level2" in md
        assert "id: final" in md
        assert "parent: level3" in md

    def test_single_node(self):
        md = serialize_tree_to_markdown(SINGLE_NODE_TREE)
        assert "id: root" in md
        assert "type: solution" in md
        assert "## Quick Fix" in md

    def test_root_has_no_parent(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        lines = md.split("\n")
        # Find the first frontmatter block
        in_first_block = False
        for line in lines:
            if line.strip() == "---":
                if not in_first_block:
                    in_first_block = True
                    continue
                else:
                    break
            if in_first_block:
                assert not line.startswith("parent:"), "Root node should not have parent"


# --- Parsing Tests ---

class TestParseMarkdownToTree:
    def test_simple_roundtrip(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        tree = result.tree_structure
        assert tree["id"] == "root"
        assert tree["type"] == "decision"
        assert tree["question"] == "Is this a test?"
        assert len(tree["options"]) == 2
        assert tree["options"][0]["label"] == "Yes"
        assert tree["options"][0]["next_node_id"] == "solution1"
        assert len(tree["children"]) == 2

    def test_action_roundtrip(self):
        md = serialize_tree_to_markdown(ACTION_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        tree = result.tree_structure
        # Find the action node
        action = tree["children"][0]
        assert action["type"] == "action"
        assert action["title"] == "Run Network Diagnostics"
        assert action["commands"] == ["ping 8.8.8.8", "tracert gateway.local"]
        assert action["expected_outcome"] == "Ping replies or timeout"
        assert action["next_node_id"] == "resolved"
        # Action should have children
        assert len(action["children"]) == 1
        assert action["children"][0]["type"] == "solution"

    def test_deeply_nested_roundtrip(self):
        md = serialize_tree_to_markdown(DEEPLY_NESTED_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        tree = result.tree_structure
        assert tree["id"] == "root"
        level2 = tree["children"][0]
        assert level2["id"] == "level2"
        level3 = level2["children"][0]
        assert level3["id"] == "level3"
        final = level3["children"][0]
        assert final["id"] == "final"
        assert final["type"] == "solution"

    def test_single_node_roundtrip(self):
        md = serialize_tree_to_markdown(SINGLE_NODE_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        tree = result.tree_structure
        assert tree["id"] == "root"
        assert tree["type"] == "solution"
        assert tree["title"] == "Quick Fix"

    def test_empty_markdown(self):
        result = parse_markdown_to_tree("")
        assert result.tree_structure is None
        assert len(result.errors) > 0

    def test_malformed_frontmatter(self):
        md = "---\nno_id_here: true\n---\n# Some question"
        result = parse_markdown_to_tree(md)
        assert any("missing 'id'" in e.message.lower() or "missing" in e.message.lower()
                    for e in result.errors)

    def test_invalid_type(self):
        md = "---\nid: root\ntype: invalid_type\n---\n# Question"
        result = parse_markdown_to_tree(md)
        assert any("invalid node type" in e.message.lower() for e in result.errors)

    def test_duplicate_ids(self):
        md = (
            "---\nid: root\ntype: decision\n---\n# Q1\n- [A] Opt → @dup\n\n"
            "---\nid: dup\ntype: solution\nparent: root\n---\n## Sol1\n\n"
            "---\nid: dup\ntype: solution\nparent: root\n---\n## Sol2\n"
        )
        result = parse_markdown_to_tree(md)
        assert any("duplicate" in e.message.lower() for e in result.errors)

    def test_broken_reference(self):
        md = (
            "---\nid: root\ntype: decision\n---\n"
            "# Which issue?\n"
            "- [A] Network → @nonexistent\n"
        )
        result = parse_markdown_to_tree(md)
        assert any("non-existent" in e.message.lower() for e in result.errors)

    def test_orphaned_parent_reference(self):
        md = (
            "---\nid: root\ntype: decision\n---\n# Q\n- [A] Opt → @child\n\n"
            "---\nid: child\ntype: solution\nparent: nonexistent_parent\n---\n## Sol\n"
        )
        result = parse_markdown_to_tree(md)
        assert any("non-existent parent" in e.message.lower() for e in result.errors)

    def test_resolution_steps_parsed(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        result = parse_markdown_to_tree(md)
        tree = result.tree_structure
        # solution1 should have resolution_steps
        sol1 = tree["children"][0]
        assert sol1["id"] == "solution1"
        assert sol1["resolution_steps"] == ["Step 1", "Step 2"]

    def test_help_text_preserved(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        result = parse_markdown_to_tree(md)
        tree = result.tree_structure
        assert tree["help_text"] == "Check if this is a test scenario"

    def test_description_with_markdown(self):
        """Test that markdown content in descriptions is preserved."""
        tree = {
            "id": "root",
            "type": "solution",
            "title": "Complex Solution",
            "description": "Use **bold** and *italic* and `code`.\n\nMultiple paragraphs too.",
            "solution": "Complex solution",
        }
        md = serialize_tree_to_markdown(tree)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        assert "**bold**" in result.tree_structure["description"]
        assert "`code`" in result.tree_structure["description"]


# --- Validation Tests ---

class TestValidateTreeMarkdown:
    def test_valid_tree_no_errors(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        errors = validate_tree_markdown(md)
        hard_errors = [e for e in errors if e.severity == "error"]
        assert len(hard_errors) == 0

    def test_empty_markdown_errors(self):
        errors = validate_tree_markdown("")
        assert len(errors) > 0

    def test_missing_solution_warning(self):
        tree = {
            "id": "root",
            "type": "decision",
            "question": "Question?",
            "options": [],
            "children": [],
        }
        md = serialize_tree_to_markdown(tree)
        errors = validate_tree_markdown(md)
        warnings = [e for e in errors if e.severity == "warning"]
        assert any("solution" in e.message.lower() for e in warnings)

    def test_empty_question_warning(self):
        tree = {
            "id": "root",
            "type": "decision",
            "question": "",
            "options": [],
            "children": [],
        }
        md = serialize_tree_to_markdown(tree)
        errors = validate_tree_markdown(md)
        assert any("empty question" in e.message.lower() for e in errors)


# --- Metadata Tests ---

class TestMetadataBlocks:
    def test_serialize_with_metadata(self):
        metadata = {"name": "Test Tree", "description": "A test", "category": "Help Desk", "tags": ["dns", "network"]}
        md = serialize_tree_to_markdown(SINGLE_NODE_TREE, metadata=metadata)
        assert "name: Test Tree" in md
        assert "description: A test" in md
        assert "category: Help Desk" in md
        assert "tags: [dns, network]" in md
        # Node should still be present
        assert "id: root" in md

    def test_parse_with_metadata(self):
        md = (
            "---\nname: My Tree\ndescription: desc here\ncategory: Help Desk\ntags: [dns, vpn]\n---\n\n"
            "---\nid: root\ntype: solution\n---\n## Quick Fix\n"
        )
        result = parse_markdown_to_tree(md)
        assert result.metadata is not None
        assert result.metadata["name"] == "My Tree"
        assert result.metadata["description"] == "desc here"
        assert result.metadata["category"] == "Help Desk"
        assert result.metadata["tags"] == ["dns", "vpn"]
        assert result.tree_structure is not None
        assert result.tree_structure["id"] == "root"

    def test_parse_without_metadata(self):
        md = "---\nid: root\ntype: solution\n---\n## Quick Fix\n"
        result = parse_markdown_to_tree(md)
        assert result.metadata is None
        assert result.tree_structure is not None

    def test_metadata_roundtrip(self):
        metadata = {"name": "Roundtrip Tree", "description": "Tests roundtrip", "tags": ["test"]}
        md = serialize_tree_to_markdown(SIMPLE_TREE, metadata=metadata)
        result = parse_markdown_to_tree(md)
        assert result.metadata is not None
        assert result.metadata["name"] == "Roundtrip Tree"
        assert result.metadata["description"] == "Tests roundtrip"
        assert result.metadata["tags"] == ["test"]
        assert result.tree_structure is not None
        assert result.tree_structure["id"] == "root"

    def test_metadata_only_no_nodes(self):
        md = "---\nname: Just Metadata\n---\n"
        result = parse_markdown_to_tree(md)
        assert result.metadata is not None
        assert result.metadata["name"] == "Just Metadata"
        assert result.tree_structure is None
        assert any("no node blocks" in e.message.lower() for e in result.errors)


# --- Round-Trip Fidelity Tests ---

class TestRoundTripFidelity:
    """Ensure serialize → parse produces semantically identical trees."""

    def _assert_node_equal(self, original: dict, parsed: dict, path: str = "root"):
        """Deep compare two node dicts, ignoring internal fields and option IDs."""
        assert original["id"] == parsed["id"], f"{path}: id mismatch"
        assert original["type"] == parsed["type"], f"{path}: type mismatch"

        if original["type"] == "decision":
            assert original.get("question", "") == parsed.get("question", ""), \
                f"{path}: question mismatch"
            # Compare option labels and next_node_ids (not auto-generated option IDs)
            orig_opts = original.get("options", [])
            parsed_opts = parsed.get("options", [])
            assert len(orig_opts) == len(parsed_opts), \
                f"{path}: options count mismatch ({len(orig_opts)} vs {len(parsed_opts)})"
            for j, (oo, po) in enumerate(zip(orig_opts, parsed_opts)):
                assert oo["label"] == po["label"], \
                    f"{path}.options[{j}]: label mismatch"
                assert oo.get("next_node_id", "") == po.get("next_node_id", ""), \
                    f"{path}.options[{j}]: next_node_id mismatch"

        elif original["type"] == "action":
            assert original.get("title", "") == parsed.get("title", ""), \
                f"{path}: title mismatch"
            assert original.get("commands", []) == parsed.get("commands", []), \
                f"{path}: commands mismatch"
            assert original.get("expected_outcome", "") == parsed.get("expected_outcome", ""), \
                f"{path}: expected_outcome mismatch"
            assert original.get("next_node_id", "") == parsed.get("next_node_id", ""), \
                f"{path}: next_node_id mismatch"

        elif original["type"] == "solution":
            assert original.get("title", "") == parsed.get("title", ""), \
                f"{path}: title mismatch"
            assert original.get("resolution_steps", []) == parsed.get("resolution_steps", []), \
                f"{path}: resolution_steps mismatch"

        # Compare children recursively
        orig_children = original.get("children", [])
        parsed_children = parsed.get("children", [])
        assert len(orig_children) == len(parsed_children), \
            f"{path}: children count mismatch ({len(orig_children)} vs {len(parsed_children)})"
        for i, (oc, pc) in enumerate(zip(orig_children, parsed_children)):
            self._assert_node_equal(oc, pc, f"{path}.children[{i}]")

    def test_simple_tree_roundtrip(self):
        md = serialize_tree_to_markdown(SIMPLE_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        self._assert_node_equal(SIMPLE_TREE, result.tree_structure)

    def test_action_tree_roundtrip(self):
        md = serialize_tree_to_markdown(ACTION_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        self._assert_node_equal(ACTION_TREE, result.tree_structure)

    def test_deeply_nested_roundtrip(self):
        md = serialize_tree_to_markdown(DEEPLY_NESTED_TREE)
        result = parse_markdown_to_tree(md)
        assert result.tree_structure is not None
        self._assert_node_equal(DEEPLY_NESTED_TREE, result.tree_structure)

    def test_double_roundtrip(self):
        """serialize → parse → serialize should produce same markdown."""
        md1 = serialize_tree_to_markdown(SIMPLE_TREE)
        result1 = parse_markdown_to_tree(md1)
        assert result1.tree_structure is not None
        md2 = serialize_tree_to_markdown(result1.tree_structure)
        result2 = parse_markdown_to_tree(md2)
        assert result2.tree_structure is not None
        self._assert_node_equal(result1.tree_structure, result2.tree_structure)


# --- API Integration Tests ---

@pytest.mark.asyncio
class TestTreeMarkdownAPI:
    async def test_export_markdown(self, client, auth_headers, test_tree):
        tree_id = test_tree["id"]
        response = await client.get(
            f"/api/v1/trees/{tree_id}/export-markdown",
            headers=auth_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert "markdown" in data
        # Should include metadata block
        assert "name:" in data["markdown"]
        # Should include node content
        assert "id: root" in data["markdown"]
        assert "# Is this a test?" in data["markdown"]

    async def test_export_not_found(self, client, auth_headers):
        import uuid
        fake_id = str(uuid.uuid4())
        response = await client.get(
            f"/api/v1/trees/{fake_id}/export-markdown",
            headers=auth_headers,
        )
        assert response.status_code == 404

    async def test_export_requires_auth(self, client, test_tree):
        tree_id = test_tree["id"]
        response = await client.get(
            f"/api/v1/trees/{tree_id}/export-markdown",
        )
        assert response.status_code == 401

    async def test_import_markdown(self, client, auth_headers, test_tree):
        tree_id = test_tree["id"]

        # Export first
        export_resp = await client.get(
            f"/api/v1/trees/{tree_id}/export-markdown",
            headers=auth_headers,
        )
        markdown = export_resp.json()["markdown"]

        # Import back
        response = await client.put(
            f"/api/v1/trees/{tree_id}/import-markdown",
            json={"markdown": markdown},
            headers=auth_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert data["valid"] is True
        assert data["tree_structure"] is not None

    async def test_import_invalid_markdown(self, client, auth_headers, test_tree):
        tree_id = test_tree["id"]
        response = await client.put(
            f"/api/v1/trees/{tree_id}/import-markdown",
            json={"markdown": "this is not valid tree markdown"},
            headers=auth_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert data["valid"] is False
        assert len(data["errors"]) > 0

    async def test_validate_markdown(self, client, auth_headers, test_tree):
        tree_id = test_tree["id"]

        # Export first
        export_resp = await client.get(
            f"/api/v1/trees/{tree_id}/export-markdown",
            headers=auth_headers,
        )
        markdown = export_resp.json()["markdown"]

        # Validate
        response = await client.post(
            "/api/v1/trees/validate-markdown",
            json={"markdown": markdown},
            headers=auth_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert data["valid"] is True
        assert data["tree_structure"] is not None
        # Should return parsed metadata from export
        assert data["metadata"] is not None

    async def test_validate_requires_auth(self, client):
        response = await client.post(
            "/api/v1/trees/validate-markdown",
            json={"markdown": "---\nid: root\ntype: solution\n---\n## Fix\n"},
        )
        assert response.status_code == 401

    async def test_roundtrip_via_api(self, client, auth_headers, test_tree):
        """Export tree → import same markdown → verify tree unchanged."""
        tree_id = test_tree["id"]

        # Export
        export_resp = await client.get(
            f"/api/v1/trees/{tree_id}/export-markdown",
            headers=auth_headers,
        )
        markdown = export_resp.json()["markdown"]

        # Import
        import_resp = await client.put(
            f"/api/v1/trees/{tree_id}/import-markdown",
            json={"markdown": markdown},
            headers=auth_headers,
        )
        assert import_resp.json()["valid"] is True

        # Re-export and compare
        export_resp2 = await client.get(
            f"/api/v1/trees/{tree_id}/export-markdown",
            headers=auth_headers,
        )
        markdown2 = export_resp2.json()["markdown"]

        # Should be identical
        assert markdown == markdown2
