using MetagraphOptimization

import MetagraphOptimization.insert_node!
import MetagraphOptimization.insert_edge!
import MetagraphOptimization.make_node
import MetagraphOptimization.siblings
import MetagraphOptimization.partners

graph = MetagraphOptimization.DAG()

@test length(graph.nodes) == 0
@test length(graph.appliedOperations) == 0
@test length(graph.operationsToApply) == 0
@test length(graph.dirtyNodes) == 0
@test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
@test length(get_operations(graph)) == (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)

# s to output (exit node)
d_exit = insert_node!(graph, make_node(DataTask(10)), track = false)

@test length(graph.nodes) == 1
@test length(graph.dirtyNodes) == 1

# final s compute
s0 = insert_node!(graph, make_node(ComputeTaskABC_S2()), track = false)

@test length(graph.nodes) == 2
@test length(graph.dirtyNodes) == 2

# data from v0 and v1 to s0
d_v0_s0 = insert_node!(graph, make_node(DataTask(5)), track = false)
d_v1_s0 = insert_node!(graph, make_node(DataTask(5)), track = false)

# v0 and v1 compute
v0 = insert_node!(graph, make_node(ComputeTaskABC_V()), track = false)
v1 = insert_node!(graph, make_node(ComputeTaskABC_V()), track = false)

# data from uB, uA, uBp and uAp to v0 and v1
d_uB_v0 = insert_node!(graph, make_node(DataTask(3)), track = false)
d_uA_v0 = insert_node!(graph, make_node(DataTask(3)), track = false)
d_uBp_v1 = insert_node!(graph, make_node(DataTask(3)), track = false)
d_uAp_v1 = insert_node!(graph, make_node(DataTask(3)), track = false)

# uB, uA, uBp and uAp computes
uB = insert_node!(graph, make_node(ComputeTaskABC_U()), track = false)
uA = insert_node!(graph, make_node(ComputeTaskABC_U()), track = false)
uBp = insert_node!(graph, make_node(ComputeTaskABC_U()), track = false)
uAp = insert_node!(graph, make_node(ComputeTaskABC_U()), track = false)

# data from PB, PA, PBp and PAp to uB, uA, uBp and uAp
d_PB_uB = insert_node!(graph, make_node(DataTask(6)), track = false)
d_PA_uA = insert_node!(graph, make_node(DataTask(6)), track = false)
d_PBp_uBp = insert_node!(graph, make_node(DataTask(6)), track = false)
d_PAp_uAp = insert_node!(graph, make_node(DataTask(6)), track = false)

# P computes PB, PA, PBp and PAp
PB = insert_node!(graph, make_node(ComputeTaskABC_P()), track = false)
PA = insert_node!(graph, make_node(ComputeTaskABC_P()), track = false)
PBp = insert_node!(graph, make_node(ComputeTaskABC_P()), track = false)
PAp = insert_node!(graph, make_node(ComputeTaskABC_P()), track = false)

# entry nodes getting data for P computes
d_PB = insert_node!(graph, make_node(DataTask(4)), track = false)
d_PA = insert_node!(graph, make_node(DataTask(4)), track = false)
d_PBp = insert_node!(graph, make_node(DataTask(4)), track = false)
d_PAp = insert_node!(graph, make_node(DataTask(4)), track = false)

@test length(graph.nodes) == 26
@test length(graph.dirtyNodes) == 26

# now for all the edges
insert_edge!(graph, d_PB, PB, track = false)
insert_edge!(graph, d_PA, PA, track = false)
insert_edge!(graph, d_PBp, PBp, track = false)
insert_edge!(graph, d_PAp, PAp, track = false)

insert_edge!(graph, PB, d_PB_uB, track = false)
insert_edge!(graph, PA, d_PA_uA, track = false)
insert_edge!(graph, PBp, d_PBp_uBp, track = false)
insert_edge!(graph, PAp, d_PAp_uAp, track = false)

insert_edge!(graph, d_PB_uB, uB, track = false)
insert_edge!(graph, d_PA_uA, uA, track = false)
insert_edge!(graph, d_PBp_uBp, uBp, track = false)
insert_edge!(graph, d_PAp_uAp, uAp, track = false)

insert_edge!(graph, uB, d_uB_v0, track = false)
insert_edge!(graph, uA, d_uA_v0, track = false)
insert_edge!(graph, uBp, d_uBp_v1, track = false)
insert_edge!(graph, uAp, d_uAp_v1, track = false)

insert_edge!(graph, d_uB_v0, v0, track = false)
insert_edge!(graph, d_uA_v0, v0, track = false)
insert_edge!(graph, d_uBp_v1, v1, track = false)
insert_edge!(graph, d_uAp_v1, v1, track = false)

insert_edge!(graph, v0, d_v0_s0, track = false)
insert_edge!(graph, v1, d_v1_s0, track = false)

insert_edge!(graph, d_v0_s0, s0, track = false)
insert_edge!(graph, d_v1_s0, s0, track = false)

insert_edge!(graph, s0, d_exit, track = false)

@test length(graph.nodes) == 26
@test length(graph.appliedOperations) == 0
@test length(graph.operationsToApply) == 0
@test length(graph.dirtyNodes) == 26
@test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)

@test is_valid(graph)

@test is_entry_node(d_PB)
@test is_entry_node(d_PA)
@test is_entry_node(d_PBp)
@test is_entry_node(d_PBp)
@test !is_entry_node(PB)
@test !is_entry_node(v0)
@test !is_entry_node(d_exit)

@test is_exit_node(d_exit)
@test !is_exit_node(d_uB_v0)
@test !is_exit_node(v0)

@test length(children(v0)) == 2
@test length(children(v1)) == 2
@test length(parents(v0)) == 1
@test length(parents(v1)) == 1

@test MetagraphOptimization.get_exit_node(graph) == d_exit

@test length(partners(s0)) == 1
@test length(siblings(s0)) == 1

operations = get_operations(graph)
@test length(operations) == (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
@test length(graph.dirtyNodes) == 0

@test sum(length(operations)) == 10

@test operations == get_operations(graph)
nf = first(operations.nodeFusions)

properties = get_properties(graph)
@test properties.computeEffort == 28
@test properties.data == 62
@test properties.computeIntensity ≈ 28 / 62
@test properties.noNodes == 26
@test properties.noEdges == 25

push_operation!(graph, nf)
# **does not immediately apply the operation**

@test length(graph.nodes) == 26
@test length(graph.appliedOperations) == 0
@test length(graph.operationsToApply) == 1
@test first(graph.operationsToApply) == nf
@test length(graph.dirtyNodes) == 0
@test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)

# this applies pending operations
properties = get_properties(graph)

@test length(graph.nodes) == 24
@test length(graph.appliedOperations) == 1
@test length(graph.operationsToApply) == 0
@test length(graph.dirtyNodes) != 0
@test properties.noNodes == 24
@test properties.noEdges == 23
@test properties.computeEffort == 28
@test properties.data < 62
@test properties.computeIntensity > 28 / 62

operations = get_operations(graph)
@test length(graph.dirtyNodes) == 0

@test length(operations) == (nodeFusions = 9, nodeReductions = 0, nodeSplits = 0)
@test !isempty(operations)

possibleNF = 9
while !isempty(operations.nodeFusions)
    push_operation!(graph, first(operations.nodeFusions))
    global operations = get_operations(graph)
    global possibleNF = possibleNF - 1
    @test length(operations) == (nodeFusions = possibleNF, nodeReductions = 0, nodeSplits = 0)
end

@test isempty(operations)

@test length(operations) == (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
@test length(graph.dirtyNodes) == 0
@test length(graph.nodes) == 6
@test length(graph.appliedOperations) == 10
@test length(graph.operationsToApply) == 0

reset_graph!(graph)

@test length(graph.dirtyNodes) == 26
@test length(graph.nodes) == 26
@test length(graph.appliedOperations) == 0
@test length(graph.operationsToApply) == 0

properties = get_properties(graph)
@test properties.noNodes == 26
@test properties.noEdges == 25
@test properties.computeEffort == 28
@test properties.data == 62
@test properties.computeIntensity ≈ 28 / 62

operations = get_operations(graph)
@test length(operations) == (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)

@test is_valid(graph)
