from unittest.mock import Mock

import pytest

from umlcharter import GraphDiagram, Mermaid
from umlcharter.charts.common import ChartingException


class TestGraphDiagram:
    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram No Nodes
---
stateDiagram-v2
""",
            ),
        ),
    )
    def test_no_nodes(self, generator_cls, output):
        gd = GraphDiagram("Diagram\nNo Nodes", generator_cls=generator_cls)
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram Only Nodes
---
stateDiagram-v2
state "Node #1" as n2
state "Node #2" as n3
""",
            ),
        ),
    )
    def test_only_nodes(self, generator_cls, output):
        gd = GraphDiagram("Diagram Only Nodes", generator_cls=generator_cls)
        gd.node("Node #1")
        gd.node("Node #2")
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram Nodes & Routes
---
stateDiagram-v2
state "Node #1" as n2
state "Node #2" as n3
n2 --> n3 : Hello
n3 --> n2 : Hi!
""",
            ),
        ),
    )
    def test_nodes_with_routes(self, generator_cls, output):
        gd = GraphDiagram("Diagram Nodes & Routes", generator_cls=generator_cls)
        n1 = gd.node("Node #1")
        n2 = gd.node("Node #2")
        n1.go_to(n2, "Hello")
        n2.go_to(n1, "Hi!")
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram Nodes & Routes
---
stateDiagram-v2
state "Node #1" as n2
state "Node #2" as n3
[*] --> n2 : Start!
n2 --> n3 : Hello!
n3 --> n2 : Hi!
n3 --> [*] : Finish!
""",
            ),
        ),
    )
    def test_start_and_finish(self, generator_cls, output):
        gd = GraphDiagram("Diagram Nodes & Routes", generator_cls=generator_cls)
        n1 = gd.node("Node #1")
        n2 = gd.node("Node #2")
        start = gd.start
        finish = gd.finish
        start.go_to(n1, "Start!")
        n1.go_to(n2, "Hello!")
        n2.go_to(n1, "Hi!")
        n2.go_to(finish, "Finish!")
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram Nested Groups
---
stateDiagram-v2
state "Group #1" as n2 {
  state "Nested Group #1" as n5 {
    state "Nested Node #1" as n8
  }
  [*] --> n5 : Go deeper!
  n5 --> [*] : It was deep, indeed
}
classDef cd_n2 fill:#769D8F
class n2 cd_n2
state "Group #2" as n9
n2 --> n9 : Inter-group route
""",
            ),
        ),
    )
    def test_nested_nodes(self, generator_cls, output):
        gd = GraphDiagram("Diagram Nested Groups", generator_cls=generator_cls)
        green_color = "769D8F"
        group1 = gd.node("Group #1", color=green_color)
        group2 = gd.node("Group #2")
        nested_group1 = group1.node("Nested Group #1", green_color)
        nested_group1.node("Nested Node #1", green_color)
        group1.go_to(group2, "Inter-group route")
        group1.start.go_to(nested_group1, "Go deeper!")
        nested_group1.go_to(group1.finish, "It was deep, indeed")
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram Joins & Forks
---
stateDiagram-v2
state "Short Path" as n2
classDef cd_n2 fill:#769D8F
class n2 cd_n2
state "Long Path #1" as n3
state "Long Path #2" as n4
state "Long Path #3" as n5
classDef cd_n5 fill:#769D8F
class n5 cd_n5
state n6 <<fork>>
state n7 <<join>>
[*] --> n6 : Choose your path
n2 --> n7 : It was easy, ha!
n3 --> n4 : 
n4 --> n5 : ... still going ...
n5 --> n7 : 
n6 --> n2 : Choose the easy one
n6 --> n3 : Choose the hard one
n7 --> [*] : 
""",
            ),
        ),
    )
    def test_join_and_fork(self, generator_cls, output):
        gd = GraphDiagram("Diagram Joins & Forks", generator_cls=generator_cls)
        green_color = "769D8F"
        n1 = gd.node("Short Path", green_color)
        n2 = gd.node("Long Path #1")
        n3 = gd.node("Long Path #2")
        n4 = gd.node("Long Path #3", color=green_color)
        fork = gd.fork()
        gd.start.go_to(fork, "Choose your path")
        fork.go_to(n1, "Choose the easy one")
        fork.go_to(n2, "Choose the hard one")
        n2.go_to(n3)
        n3.go_to(n4, "... still going ...")
        join = gd.join()
        n4.go_to(join)
        n1.go_to(join, "It was easy, ha!")
        join.go_to(gd.finish)
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Diagram Conditions
---
stateDiagram-v2
state "Node #1" as n2
state n3 <<choice>>
state "Node (conditional) #2" as n4
[*] --> n2 : Go to first
n2 --> n3 : Make a choice
n3 --> n4 : Need a second one
n3 --> [*] : Nah, try again
n4 --> [*] : 
""",
            ),
        ),
    )
    def test_condition(self, generator_cls, output):
        gd = GraphDiagram("Diagram Conditions", generator_cls=generator_cls)
        n1 = gd.node("Node #1")
        c1 = gd.condition()
        n2 = gd.node("Node (conditional) #2")
        gd.start.go_to(n1, "Go to first")
        n1.go_to(c1, "Make a choice")
        c1.go_to(n2, "Need a second one")
        n2.go_to(gd.finish)
        c1.go_to(gd.finish, "Nah, try again")
        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,output",
        (
            (
                Mermaid,
                """---
title: Complex Diagram With Notes
---
stateDiagram-v2
state "Group" as n2 {
  state "Nested Node" as n5
  note right of n5
Note for the nested node
  end note
}
note right of n2
Note for the group
end note
state "Outer Node" as n6
note right of n6
It is possible to have multiple notes...
end note
note right of n6
...and also split them
in multi lines
end note
state n7 <<fork>>
note right of n7
Note for fork
end note
state n8 <<choice>>
note right of n8
Note for condition
end note
state n9 <<join>>
note right of n9
Note for join
end note
n2 --> n7 : 
n7 --> n8 : 
n7 --> n9 : 
n8 --> n9 : 
n8 --> [*] : 
""",
            ),
        ),
    )
    def test_complex_with_notes(self, generator_cls, output):
        gd = GraphDiagram("Complex Diagram With Notes", generator_cls=generator_cls)
        group = gd.node("Group")
        nested_node = group.node("Nested Node")
        node = gd.node("Outer Node")
        fork = gd.fork()
        condition = gd.condition()
        join = gd.join()

        group.go_to(fork)
        fork.go_to(condition)
        condition.go_to(join)
        condition.go_to(gd.finish)
        fork.go_to(join)

        group.note("Note for the group")
        nested_node.note("Note for the nested node")
        node.note("It is possible to have multiple notes...")
        node.note("...and also split them\nin multi lines")

        fork.note("Note for fork")
        condition.note("Note for condition")
        join.note("Note for join")

        assert str(gd) == output

    @pytest.mark.parametrize(
        "generator_cls,is_vertical,output",
        (
            (
                Mermaid,
                True,
                """---
title: Diagram Different Orientation
---
stateDiagram-v2
state "Node: #1" as n2
state "Node: #2" as n3
state "Node: #3" as n4
state "Node: #4" as n5
n2 --> n3 : 
n3 --> n4 : 
n4 --> n5 : 
n5 --> n2 : We are rolling yay!
""",
            ),
            (
                Mermaid,
                False,
                """---
title: Diagram Different Orientation
---
stateDiagram-v2
direction LR
state "Node: #1" as n2
state "Node: #2" as n3
state "Node: #3" as n4
state "Node: #4" as n5
n2 --> n3 : 
n3 --> n4 : 
n4 --> n5 : 
n5 --> n2 : We are rolling yay!
""",
            ),
        ),
    )
    def test_orientation(self, generator_cls, is_vertical, output):
        gd = GraphDiagram(
            "Diagram: Different Orientation",
            generator_cls=generator_cls,
            is_vertical=is_vertical,
        )
        n1 = gd.node("Node: #1")
        n2 = gd.node("Node: #2")
        n1.go_to(n2)
        n3 = gd.node("Node: #3")
        n2.go_to(n3)
        n4 = gd.node("Node: #4")
        n3.go_to(n4)
        n4.go_to(n1, "We are rolling: yay!")
        assert str(gd) == output

    def test_invalid_color_string(self):
        gd = GraphDiagram("Invalid color", Mock)
        with pytest.raises(ChartingException):
            gd.node("Invalid color", "red")

    def test_same_titles_on_the_same_level(self):
        gd = GraphDiagram("Same titles", Mock)
        gd.node("Title")
        with pytest.raises(ChartingException):
            gd.node("Title")

    def test_cannot_interact_between_levels(self):
        gd = GraphDiagram("Different levels", Mock)
        outer_node = gd.node("Outer")
        nested_node = gd.node("Group").node("Nested")
        with pytest.raises(ChartingException):
            outer_node.go_to(nested_node)

    def test_cannot_build_same_graph_edge_twice(self):
        gd = GraphDiagram("Duplicated graph edges", Mock)
        n1 = gd.node("Node #1")
        n2 = gd.node("Node #2")
        n1.go_to(n2)
        with pytest.raises(ChartingException):
            n1.go_to(n2)

    def test_cannot_go_to_abstract_start(self):
        gd = GraphDiagram("Start is not a destination", Mock)
        n1 = gd.node("Node #1")
        with pytest.raises(ChartingException):
            n1.go_to(gd.start)

    def test_cannot_go_from_abstract_finish(self):
        gd = GraphDiagram("Finish is not a strtating point", Mock)
        n1 = gd.node("Node #1")
        with pytest.raises(ChartingException):
            gd.finish.go_to(n1)

    def test_cannot_directly_link_start_and_finish(self):
        gd = GraphDiagram("Directly link start and finish", Mock)
        with pytest.raises(ChartingException):
            gd.start.go_to(gd.finish)
