"""
Tests for mermaid_graph: parser, serializer, converter, and round-trips.
"""

from __future__ import annotations

import textwrap
import warnings

import networkx as nx
import pytest

import mermaid_graph as mnx


# ════════════════════════════════════════════════════════════════════════
#  Test data
# ════════════════════════════════════════════════════════════════════════

SIMPLE_FLOWCHART = textwrap.dedent("""\
    flowchart TD
        A([Start]) --> B{Decision}
        B -->|Yes| C[OK]
        B -.->|No| D[Fail]
""")

SUBGRAPH_FLOWCHART = textwrap.dedent("""\
    flowchart LR
        classDef primary fill:#f9f,stroke:#333
        classDef secondary fill:#bbf,stroke:#339

        subgraph auth["Authentication"]
            A([Start]):::primary --> B{Logged in?}
            B -->|Yes| C[Dashboard]:::primary
            B -.->|No| D[Login Page]:::secondary
        end

        subgraph main["Main Flow"]
            C --> E[[Subroutine]]
            E ==> F[(Database)]
        end

        style A fill:#ff0,stroke:#000
        linkStyle 0 stroke:red,stroke-width:3px
""")

NESTED_SUBGRAPH = textwrap.dedent("""\
    flowchart TD
        subgraph outer["Outer"]
            subgraph inner["Inner"]
                X[NodeX] --> Y[NodeY]
            end
            Z[NodeZ] --> X
        end
""")


# ════════════════════════════════════════════════════════════════════════
#  Parser tests
# ════════════════════════════════════════════════════════════════════════

def test_parse_simple():
    data = mnx.mermaid_to_json(SIMPLE_FLOWCHART)

    assert data["directed"] is True
    assert data["graph"]["diagram_type"] == "flowchart"
    assert data["graph"]["direction"] == "TD"

    node_ids = {n["id"] for n in data["nodes"]}
    assert node_ids == {"A", "B", "C", "D"}

    # Check shapes
    nodes_by_id = {n["id"]: n for n in data["nodes"]}
    assert nodes_by_id["A"]["shape"] == "stadium"
    assert nodes_by_id["B"]["shape"] == "diamond"
    assert nodes_by_id["C"]["shape"] == "rect"

    # Check labels
    assert nodes_by_id["A"].get("label") == "Start"
    assert nodes_by_id["B"].get("label") == "Decision"

    # Check edges
    assert len(data["links"]) == 3
    links_by_idx = {lk["link_index"]: lk for lk in data["links"]}
    assert links_by_idx[0]["source"] == "A"
    assert links_by_idx[0]["target"] == "B"
    assert links_by_idx[1]["label"] == "Yes"
    assert links_by_idx[2]["line_type"] == "dotted"


def test_parse_subgraphs():
    data = mnx.mermaid_to_json(SUBGRAPH_FLOWCHART)
    meta = data["graph"]

    # classDefs
    assert "primary" in meta["class_defs"]
    assert meta["class_defs"]["primary"]["fill"] == "#f9f"

    # subgraphs
    sg_ids = {sg["id"] for sg in meta["subgraphs"]}
    assert sg_ids == {"auth", "main"}

    auth_sg = next(sg for sg in meta["subgraphs"] if sg["id"] == "auth")
    assert auth_sg["label"] == "Authentication"
    assert "A" in auth_sg["nodes"]
    assert "B" in auth_sg["nodes"]

    # linkStyle
    assert "0" in meta.get("link_styles", {})

    # inline style
    node_a = next(n for n in data["nodes"] if n["id"] == "A")
    assert node_a.get("style", {}).get("fill") == "#ff0"

    # css_class
    assert node_a.get("css_class") == "primary"


def test_parse_nested_subgraphs():
    data = mnx.mermaid_to_json(NESTED_SUBGRAPH)
    meta = data["graph"]

    assert len(meta["subgraphs"]) == 1  # only "outer" at top level
    outer = meta["subgraphs"][0]
    assert outer["id"] == "outer"
    assert "Z" in outer["nodes"]

    assert len(outer["subgraphs"]) == 1
    inner = outer["subgraphs"][0]
    assert inner["id"] == "inner"
    assert "X" in inner["nodes"]
    assert "Y" in inner["nodes"]


def test_parse_edge_types():
    text = textwrap.dedent("""\
        flowchart TD
            A --- B
            B --> C
            C -.-> D
            D -.- E
            E ==> F
            F === G
    """)
    data = mnx.mermaid_to_json(text)
    links = sorted(data["links"], key=lambda link: link["link_index"])

    expected = [
        ("solid",  "none"),    # ---
        ("solid",  "normal"),  # -->
        ("dotted", "normal"),  # -.->
        ("dotted", "none"),    # -.-
        ("thick",  "normal"),  # ==>
        ("thick",  "none"),    # ===
    ]
    for link, (lt, at) in zip(links, expected):
        assert link["line_type"] == lt, f"Expected {lt} got {link['line_type']} for index {link['link_index']}"
        assert link["arrow"] == at, f"Expected {at} got {link['arrow']} for index {link['link_index']}"


def test_parse_circle_cross_arrows():
    """--o and --x arrows are correctly classified (A1 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            A --o B
            C --x D
    """)
    data = mnx.mermaid_to_json(text)
    links = sorted(data["links"], key=lambda link: link["link_index"])

    assert links[0]["source"] == "A"
    assert links[0]["target"] == "B"
    assert links[0]["arrow"] == "circle"
    assert links[0]["line_type"] == "solid"

    assert links[1]["source"] == "C"
    assert links[1]["target"] == "D"
    assert links[1]["arrow"] == "cross"
    assert links[1]["line_type"] == "solid"


def test_parse_inline_label():
    """-- text --> style inline labels (A2 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            A -- label text --> B
    """)
    data = mnx.mermaid_to_json(text)
    assert len(data["links"]) == 1
    link = data["links"][0]
    assert link["source"] == "A"
    assert link["target"] == "B"
    assert link["label"] == "label text"
    assert link["arrow"] == "normal"


def test_parse_hyphenated_node_ids():
    """Hyphenated node IDs like my-node are supported (A4 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            my-node[First] --> other-node[Second]
    """)
    data = mnx.mermaid_to_json(text)
    node_ids = {n["id"] for n in data["nodes"]}
    assert node_ids == {"my-node", "other-node"}

    nodes_by_id = {n["id"]: n for n in data["nodes"]}
    assert nodes_by_id["my-node"]["label"] == "First"
    assert nodes_by_id["other-node"]["label"] == "Second"


def test_parse_class_before_node_definition():
    """class assignment works even if nodes are defined later (A6 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            classDef highlight fill:#ff0
            class A,B highlight
            A --> B
    """)
    data = mnx.mermaid_to_json(text)
    nodes_by_id = {n["id"]: n for n in data["nodes"]}
    assert nodes_by_id["A"]["css_class"] == "highlight"
    assert nodes_by_id["B"]["css_class"] == "highlight"


def test_parse_quoted_label_with_brackets():
    """Quoted labels strip surrounding quotes (A7 partial fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            A["Label with parens"]
    """)
    data = mnx.mermaid_to_json(text)
    node_a = next(n for n in data["nodes"] if n["id"] == "A")
    assert node_a["label"] == "Label with parens"


def test_parse_unclosed_subgraph_warns():
    """Unclosed subgraph emits a warning and is still included (B8 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            subgraph sg1
                A --> B
    """)
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        data = mnx.mermaid_to_json(text)
        assert len(w) == 1
        assert "Unclosed" in str(w[0].message)

    # Subgraph should still be present
    assert len(data["graph"]["subgraphs"]) == 1
    assert data["graph"]["subgraphs"][0]["id"] == "sg1"


def test_parse_unsupported_diagram_type():
    """Non-flowchart diagram types raise MermaidParseError (B9 fix)."""
    with pytest.raises(mnx.MermaidParseError, match="sequenceDiagram"):
        mnx.mermaid_to_json("sequenceDiagram\n    Alice->>Bob: Hello")


def test_parse_parallel_edges_detected():
    """Parallel edges set multigraph flag (A5 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            A -->|first| B
            A -->|second| B
    """)
    data = mnx.mermaid_to_json(text)
    assert data["multigraph"] is True
    assert len(data["links"]) == 2


def test_parse_unicode_labels():
    """Unicode (e.g. Japanese) labels are handled correctly."""
    text = textwrap.dedent("""\
        flowchart TD
            A[開始] --> B[終了]
    """)
    data = mnx.mermaid_to_json(text)
    nodes_by_id = {n["id"]: n for n in data["nodes"]}
    assert nodes_by_id["A"]["label"] == "開始"
    assert nodes_by_id["B"]["label"] == "終了"


def test_parse_chained_edges():
    """Chained edges like A --> B --> C are supported."""
    text = textwrap.dedent("""\
        flowchart TD
            A --> B --> C --> D
    """)
    data = mnx.mermaid_to_json(text)
    assert len(data["nodes"]) == 4
    assert len(data["links"]) == 3

    edges = [(lk["source"], lk["target"]) for lk in data["links"]]
    assert ("A", "B") in edges
    assert ("B", "C") in edges
    assert ("C", "D") in edges


def test_parse_empty_input():
    """Empty input returns a valid default graph."""
    data = mnx.mermaid_to_json("")
    assert data["directed"] is True
    assert data["nodes"] == []
    assert data["links"] == []


def test_parse_no_header():
    """Input without flowchart/graph header still parses edges."""
    data = mnx.mermaid_to_json("A --> B")
    assert len(data["nodes"]) == 2
    assert len(data["links"]) == 1


# ════════════════════════════════════════════════════════════════════════
#  Serializer tests
# ════════════════════════════════════════════════════════════════════════

def test_serialize_simple():
    data = mnx.mermaid_to_json(SIMPLE_FLOWCHART)
    text = mnx.json_to_mermaid(data)

    assert "flowchart TD" in text
    assert "A([Start])" in text or "A([ Start ])" in text.replace(" ", "")
    assert "-->|Yes|" in text
    assert "-.->" in text or "-.-> " in text


def test_serialize_subgraphs():
    data = mnx.mermaid_to_json(SUBGRAPH_FLOWCHART)
    text = mnx.json_to_mermaid(data)

    assert "classDef primary" in text
    assert "subgraph auth" in text
    assert "subgraph main" in text
    assert "end" in text
    assert "style A" in text
    assert "linkStyle 0" in text


def test_serialize_nested_subgraphs():
    data = mnx.mermaid_to_json(NESTED_SUBGRAPH)
    text = mnx.json_to_mermaid(data)

    # Verify nesting structure
    lines = text.splitlines()
    outer_line = next(i for i, line in enumerate(lines) if "subgraph outer" in line)
    inner_line = next(i for i, line in enumerate(lines) if "subgraph inner" in line)
    assert inner_line > outer_line


def test_serialize_circle_cross_arrows():
    """--o and --x are serialized correctly."""
    data = {
        "directed": True, "multigraph": False,
        "graph": {"diagram_type": "flowchart", "direction": "TD"},
        "nodes": [{"id": "A", "shape": "rect"}, {"id": "B", "shape": "rect"}],
        "links": [
            {"source": "A", "target": "B", "line_type": "solid", "arrow": "circle"},
        ],
    }
    text = mnx.json_to_mermaid(data)
    assert "--o" in text


def test_serialize_no_links():
    """Graph with no links does not produce a trailing blank line."""
    data = {
        "directed": True, "multigraph": False,
        "graph": {"diagram_type": "flowchart", "direction": "TD"},
        "nodes": [{"id": "A", "shape": "rect"}],
        "links": [],
    }
    text = mnx.json_to_mermaid(data)
    assert not text.endswith("\n\n")


# ════════════════════════════════════════════════════════════════════════
#  NetworkX converter tests
# ════════════════════════════════════════════════════════════════════════

def test_to_networkx():
    data = mnx.mermaid_to_json(SUBGRAPH_FLOWCHART)
    G = mnx.to_networkx(data)

    assert isinstance(G, (nx.DiGraph, nx.Graph))
    assert "A" in G.nodes
    assert "B" in G.nodes
    assert G.has_edge("A", "B")

    # Node attributes
    assert G.nodes["A"]["shape"] == "stadium"
    assert G.nodes["A"]["css_class"] == "primary"

    # Subgraph membership
    assert G.nodes["A"]["_subgraph_path"] == ["auth"]
    assert G.nodes["E"]["_subgraph_path"] == ["main"]

    # Graph-level metadata
    assert G.graph["diagram_type"] == "flowchart"
    assert "primary" in G.graph.get("class_defs", {})

    # Standard NetworkX operations work
    path = nx.shortest_path(G, "A", "C")
    assert path == ["A", "B", "C"]


def test_to_networkx_nested():
    data = mnx.mermaid_to_json(NESTED_SUBGRAPH)
    G = mnx.to_networkx(data)

    assert G.nodes["X"]["_subgraph_path"] == ["outer", "inner"]
    assert G.nodes["Z"]["_subgraph_path"] == ["outer"]


def test_to_networkx_multigraph():
    """Parallel edges are preserved when multigraph is True (A5 fix)."""
    text = textwrap.dedent("""\
        flowchart TD
            A -->|first| B
            A -->|second| B
    """)
    data = mnx.mermaid_to_json(text)
    assert data["multigraph"] is True

    G = mnx.to_networkx(data)
    assert isinstance(G, nx.MultiDiGraph)
    # Both edges should be preserved
    assert G.number_of_edges("A", "B") == 2


def test_to_networkx_does_not_mutate_input():
    """to_networkx should not mutate the input data dict."""
    data = mnx.mermaid_to_json(SIMPLE_FLOWCHART)
    import copy
    original = copy.deepcopy(data)
    mnx.to_networkx(data)
    assert data == original


def test_from_networkx():
    data = mnx.mermaid_to_json(SUBGRAPH_FLOWCHART)
    G = mnx.to_networkx(data)
    recovered = mnx.from_networkx(G)

    # Subgraphs should be reconstructed
    sg_ids = {sg["id"] for sg in recovered["graph"].get("subgraphs", [])}
    assert "auth" in sg_ids
    assert "main" in sg_ids

    # Nodes should not have _subgraph_path in the dict output
    for node in recovered["nodes"]:
        assert "_subgraph_path" not in node

    # class_defs preserved
    assert "primary" in recovered["graph"].get("class_defs", {})


def test_from_networkx_plain_graph():
    """from_networkx works with a plain graph that has no Mermaid metadata."""
    G = nx.DiGraph()
    G.add_node("A")
    G.add_node("B")
    G.add_edge("A", "B")
    G.graph["diagram_type"] = "flowchart"

    data = mnx.from_networkx(G)
    assert "links" in data
    assert len(data["links"]) == 1


def test_from_networkx_subgraph_label_roundtrip():
    """Subgraph labels survive the full NX round-trip (B10 fix)."""
    text = textwrap.dedent("""\
        flowchart LR
            subgraph auth["Authentication Flow"]
                A --> B
            end
    """)
    data = mnx.mermaid_to_json(text)
    G = mnx.to_networkx(data)
    recovered = mnx.from_networkx(G)

    sg = next(sg for sg in recovered["graph"]["subgraphs"] if sg["id"] == "auth")
    assert sg["label"] == "Authentication Flow"


def test_from_networkx_rebuild_subgraph_label():
    """When subgraphs are rebuilt from _subgraph_path + _subgraph_labels."""
    G = nx.DiGraph()
    G.add_node("A", _subgraph_path=["auth"], label="Start", shape="stadium")
    G.add_node("B", _subgraph_path=["auth"], label="End")
    G.add_edge("A", "B")
    G.graph["diagram_type"] = "flowchart"
    G.graph["_subgraph_labels"] = {"auth": "Authentication"}

    data = mnx.from_networkx(G)
    sg = data["graph"]["subgraphs"][0]
    assert sg["id"] == "auth"
    assert sg["label"] == "Authentication"
    # style: None should not be emitted (B11 fix)
    assert "style" not in sg


def test_networkx_graph_analysis():
    """Verify standard NetworkX analysis works on converted graphs."""
    G = mnx.mermaid_to_networkx(SIMPLE_FLOWCHART)

    # In-degree / out-degree
    assert G.in_degree("B") >= 1
    assert G.out_degree("B") == 2

    # Successors
    successors = set(G.successors("B"))
    assert successors == {"C", "D"}

    # Subgraph extraction
    sub = G.subgraph(["A", "B", "C"])
    assert len(sub.nodes) == 3


# ════════════════════════════════════════════════════════════════════════
#  Round-trip tests
# ════════════════════════════════════════════════════════════════════════

def test_roundtrip_json():
    """Mermaid → JSON → Mermaid → JSON: structural equivalence."""
    data1 = mnx.mermaid_to_json(SIMPLE_FLOWCHART)
    text = mnx.json_to_mermaid(data1)
    data2 = mnx.mermaid_to_json(text)

    # Same nodes
    ids1 = {n["id"] for n in data1["nodes"]}
    ids2 = {n["id"] for n in data2["nodes"]}
    assert ids1 == ids2

    # Same edge count
    assert len(data1["links"]) == len(data2["links"])

    # Same edge topology
    edges1 = {(link["source"], link["target"]) for link in data1["links"]}
    edges2 = {(link["source"], link["target"]) for link in data2["links"]}
    assert edges1 == edges2


def test_roundtrip_networkx():
    """Mermaid → NX → JSON → Mermaid: topology preserved."""
    G = mnx.mermaid_to_networkx(SUBGRAPH_FLOWCHART)
    text = mnx.networkx_to_mermaid(G)
    G2 = mnx.mermaid_to_networkx(text)

    assert set(G.nodes) == set(G2.nodes)
    assert set(G.edges) == set(G2.edges)


def test_roundtrip_nested_subgraph():
    """Nested subgraph structure survives full round-trip."""
    data1 = mnx.mermaid_to_json(NESTED_SUBGRAPH)
    G = mnx.to_networkx(data1)
    data2 = mnx.from_networkx(G)

    outer = next(sg for sg in data2["graph"]["subgraphs"] if sg["id"] == "outer")
    assert "Z" in outer["nodes"]
    inner = next(sg for sg in outer["subgraphs"] if sg["id"] == "inner")
    assert "X" in inner["nodes"]
    assert "Y" in inner["nodes"]


def test_roundtrip_full():
    """Complete round-trip: Mermaid → JSON → NX → JSON → Mermaid → JSON."""
    data1 = mnx.mermaid_to_json(SUBGRAPH_FLOWCHART)
    G = mnx.to_networkx(data1)
    data2 = mnx.from_networkx(G)
    text = mnx.json_to_mermaid(data2)
    data3 = mnx.mermaid_to_json(text)

    # Topology preserved
    ids1 = {n["id"] for n in data1["nodes"]}
    ids3 = {n["id"] for n in data3["nodes"]}
    assert ids1 == ids3

    edges1 = {(link["source"], link["target"]) for link in data1["links"]}
    edges3 = {(link["source"], link["target"]) for link in data3["links"]}
    assert edges1 == edges3


# ════════════════════════════════════════════════════════════════════════
#  Schema / dataclass tests
# ════════════════════════════════════════════════════════════════════════

def test_schema_roundtrip():
    """MermaidGraph.from_dict(data.to_dict()) preserves structure."""
    graph = mnx.MermaidGraph(
        diagram_type="flowchart",
        direction="LR",
        class_defs={"highlight": {"fill": "#ff0"}},
        subgraphs=[mnx.Subgraph(id="sg1", label="Group 1", nodes=["A", "B"])],
        nodes=[
            mnx.MermaidNode(id="A", label="Node A", shape="diamond"),
            mnx.MermaidNode(id="B", label="Node B"),
        ],
        links=[mnx.MermaidLink(source="A", target="B", label="go")],
    )
    d = graph.to_dict()
    recovered = mnx.MermaidGraph.from_dict(d)

    assert recovered.direction == "LR"
    assert len(recovered.subgraphs) == 1
    assert recovered.subgraphs[0].id == "sg1"
    assert recovered.nodes[0].shape == "diamond"
    assert recovered.links[0].label == "go"


def test_schema_validation_invalid_shape():
    """MermaidNode raises MermaidValidationError for invalid shape."""
    with pytest.raises(mnx.MermaidValidationError, match="Unknown shape"):
        mnx.MermaidNode(id="A", shape="invalid")


def test_schema_validation_invalid_line_type():
    """MermaidLink raises MermaidValidationError for invalid line_type."""
    with pytest.raises(mnx.MermaidValidationError, match="Unknown line_type"):
        mnx.MermaidLink(source="A", target="B", line_type="wavy")


def test_schema_validation_invalid_arrow():
    """MermaidLink raises MermaidValidationError for invalid arrow."""
    with pytest.raises(mnx.MermaidValidationError, match="Unknown arrow"):
        mnx.MermaidLink(source="A", target="B", arrow="fancy")


def test_schema_validation_invalid_direction():
    """MermaidGraph raises MermaidValidationError for invalid direction."""
    with pytest.raises(mnx.MermaidValidationError, match="Unknown direction"):
        mnx.MermaidGraph(direction="XX")


def test_link_styles_key_always_str():
    """link_styles keys are normalised to str in to_dict / from_dict."""
    graph = mnx.MermaidGraph()
    graph.link_styles[0] = {"stroke": "red"}  # type: ignore[index]
    d = graph.to_dict()
    assert all(isinstance(k, str) for k in d["graph"]["link_styles"])

    recovered = mnx.MermaidGraph.from_dict(d)
    assert all(isinstance(k, str) for k in recovered.link_styles)


# ════════════════════════════════════════════════════════════════════════
#  Version / metadata tests
# ════════════════════════════════════════════════════════════════════════

def test_version():
    assert hasattr(mnx, "__version__")
    assert isinstance(mnx.__version__, str)
