module SimulatorTests

open CommonTypes
open SimulatorTypes
open CanvasStates
open CanvasStatesWithBuses
open Simulator

/// Tuple with: (diagramName, state, loadedComponents, inputs).
type private SimulatorTestCaseInput = string * CanvasState * LoadedComponent list * (ComponentId * WireData) list
type private SimulatorTestCaseOutput = Result<(SimulationIO * WireData) list, SimulationError>
type private SimulatorTestCase = string * SimulatorTestCaseInput * SimulatorTestCaseOutput

let private makeError msg deps comps conns =
    Error {
        Msg = msg
        InDependency = deps
        ComponentsAffected = comps |> List.map ComponentId
        ConnectionsAffected = conns |> List.map ConnectionId
    }

/// Given a list of N generic elements, associate each element with a bit and
/// return 2^N lists with all the possible bit combinations.
/// A bit is simply a bus with width 1.
let makeAllBitCombinations (lst : 'a list) : (('a * WireData) list) list =
    let rec allCombinations lst result stack =
        match lst with
        | [] -> List.rev stack :: result
        | el :: lst' ->
            let result = allCombinations lst' result ((el,[Zero]) :: stack)
            allCombinations lst' result ((el,[One]) :: stack)
    List.rev <| allCombinations lst [] []

/// Auto generate all the testcases for a CanvasState (i.e. a full thruth
/// table). The thruth table considers ALL inputs to be single bit inputs,
/// please do not use this function if not all inputs are like that. 
let private createAllTestCases
        (title : string)
        (diagramName : string)
        (state : CanvasState)
        (dependencies : LoadedComponent list)
        (inputLabels : ComponentId list)
        (expectedResults : ((SimulationIO * WireData) list) list)
        : SimulatorTestCase list =
    let allInputCombinations = makeAllBitCombinations inputLabels
    assert(List.length allInputCombinations = List.length expectedResults)
    (allInputCombinations, expectedResults)
    ||> List.map2 (fun inputs outputs ->
        sprintf "%s: %A" title inputs,
        (diagramName, state, dependencies, inputs),
        Ok outputs
    )

// The input to a test case is formed by:
// - a CanvasState,
// - a list of loaded dependencies,
// - a list of input values that will be applied to the simulation graph.

// The dependency list and the inputs do not matter since the test has to fail
// in the earlier checks.
let private testCasesSimulatorPortError : SimulatorTestCase list = [
    "Unconnected input node",
    ("", state1, [], []),
    makeError
        "All ports must have at least one connection."
        None
        ["input-node0"]
        []

    "Two unconnected input nodes",
    ("", state2, [], []),
    makeError
        "All ports must have at least one connection."
        None
        ["input-node0"]
        []

    "Two inputs and one output",
    ("", state5, [], []),
    makeError
        "A wire must have precisely one driving component, but 2 were found. If you want to merge wires together, use a MergeWires component."
        None
        ["output-node0"]
        []

    "Two inputs, one And, one output, with extra connection input to output",
    ("", state7, [], []),
    makeError
        "A wire must have precisely one driving component, but 2 were found. If you want to merge wires together, use a MergeWires component."
        None
        ["output"]
        []

    "Two inputs, one And, one output, with extra connections inputs to and",
    ("", state8, [], []),
    makeError
        "A wire must have precisely one driving component, but 2 were found. If you want to merge wires together, use a MergeWires component."
        None
        ["and"]
        []

    "Mux2 with only two connected ports",
    ("", state9, [], []),
    makeError
        "All ports must have at least one connection."
        None
        ["mux"]
        []
]

let private testCasesSimulatorDuplicatIOError : SimulatorTestCase list = [
    "Simple circuit with duplicated output label",
    ("", state14, [], []),
    makeError
        "Two Output components cannot have the same label: output-duplicate-label."
        None
        ["output-node0"; "output-node1"]
        []

    "Simple And circuit with duplicated input label",
    ("", state15, [], []),
    makeError
        "Two Input components cannot have the same label: input-duplicate-label."
        None
        ["top-input"; "bottom-input"]
        []
]

let private testCasesSimulatorCycleError : SimulatorTestCase list = [
    "Complex diagram with three Ands and two cycles",
    ("", state10, [], []),
    makeError
        "Cycle detected in combinatorial logic."
        None
        ["and2"; "and0"]
        ["conn5"; "conn4"]

    "Complex diagram with three Ands and one long cycle",
    ("", state11, [], []),
    makeError
        "Cycle detected in combinatorial logic."
        None
        ["and1"; "and2"; "and0"]
        ["conn1"; "conn5"; "conn3"]
]

// In the next tests, we have no dependencies.
// The inputs are set since the simulation graph should be fine.
let private testCasesSimulatorOkNoDependencies : SimulatorTestCase list =
    createAllTestCases
        "Simple circuit with one input and one output"
        "main" state3 [] [ComponentId "input-node0"]
        [
           [(ComponentId "output-node0", ComponentLabel "output-node0-label", 1), [Zero]]
           [(ComponentId "output-node0", ComponentLabel "output-node0-label", 1), [One]]
        ]
    @
    createAllTestCases
        "Simple circuit with one input connected to two outputs"
        "main" state4 [] [ComponentId "input-node0"]
        [
            [
                (ComponentId "output-node0", ComponentLabel "output-node0-label", 1), [Zero]
                (ComponentId "output-node1", ComponentLabel "output-node1-label", 1), [Zero]
            ]
            [
                (ComponentId "output-node0", ComponentLabel "output-node0-label", 1), [One]
                (ComponentId "output-node1", ComponentLabel "output-node1-label", 1), [One]
            ]
        ]
    @
    createAllTestCases
        "Two inputs; one And; one output"
        "main" state6 [] [ComponentId "top-input"; ComponentId "bottom-input"]
        [
            [(ComponentId "output", ComponentLabel "output-node0-label", 1), [Zero]]
            [(ComponentId "output", ComponentLabel "output-node0-label", 1), [Zero]]
            [(ComponentId "output", ComponentLabel "output-node0-label", 1), [Zero]]
            [(ComponentId "output", ComponentLabel "output-node0-label", 1), [One]]
        ]
    @
    createAllTestCases
        "Weird diagram with a series of and gates"
        "main" state12 [] [ComponentId "input"]
        [
            [(ComponentId "output", ComponentLabel "output", 1), [Zero]]
            [(ComponentId "output", ComponentLabel "output", 1), [One]]
        ]
    @
    createAllTestCases
        "One bit adder (Zero, Zero)"
        "main" state13 [] [
            ComponentId "2953603d-44e4-5c1f-3fb1-698f7863b6b5"
            ComponentId "170e69f4-b3d7-d9e0-9f1d-6a564ba62062"
        ]
        [
            [
                (ComponentId "9aaf18a9-b3ac-bf51-1ed3-625baa1ff6a9", ComponentLabel "Sum", 1), [Zero]
                (ComponentId "94da6dd7-a263-a3ec-ec76-bfa07b0b0f34", ComponentLabel "Carry", 1), [Zero]
            ]
            [
                (ComponentId "9aaf18a9-b3ac-bf51-1ed3-625baa1ff6a9", ComponentLabel "Sum", 1), [One]
                (ComponentId "94da6dd7-a263-a3ec-ec76-bfa07b0b0f34", ComponentLabel "Carry", 1), [Zero]
            ]
            [
                (ComponentId "9aaf18a9-b3ac-bf51-1ed3-625baa1ff6a9", ComponentLabel "Sum", 1), [One]
                (ComponentId "94da6dd7-a263-a3ec-ec76-bfa07b0b0f34", ComponentLabel "Carry", 1), [Zero]
            ]
            [
                (ComponentId "9aaf18a9-b3ac-bf51-1ed3-625baa1ff6a9", ComponentLabel "Sum", 1), [Zero]
                (ComponentId "94da6dd7-a263-a3ec-ec76-bfa07b0b0f34", ComponentLabel "Carry", 1), [One]
            ]
        ]

let testCasesSimulatorDependencyError : SimulatorTestCase list =
    createAllTestCases
        "Broken unused dependency." // Since the dependency is unused the test should pass.
        "main" state3 [state1Dependency] [ComponentId "input-node0"]
        [
           [(ComponentId "output-node0", ComponentLabel "output-node0-label", 1), [Zero]]
           [(ComponentId "output-node0", ComponentLabel "output-node0-label", 1), [One]]
        ]
    @
    [
        // Broken dependencies.

        "Input connected to broken depdendency.",
        ("main", state19, [state1Dependency], []),
        makeError
            "All ports must have at least one connection."
            (Some "broken-one-input") [] []

        // Dependency cycle.

        "Component using itself.",
        (state16Dependency.Name, state20, [state16Dependency], []),
        makeError
            (sprintf "Found a cycle in dependencies: \"%s\" --> \"%s\"." state16Dependency.Name state16Dependency.Name)
            None [] []

        "Long cycle starting at root.",
        (state23Dependency.Name, state23, [state21Dependency; state22Dependency], []),
        makeError
            (sprintf "Found a cycle in dependencies: \"%s\" --> \"%s\" --> \"%s\" --> \"%s\"." state23Dependency.Name state21Dependency.Name state22Dependency.Name state23Dependency.Name)
            None [] []

        "Long cycle.",
        ("main", state24, [state21Dependency; state22Dependency; state23Dependency], []),
        makeError
            (sprintf "Found a cycle in dependencies: \"%s\" --> \"%s\" --> \"%s\" --> \"%s\"." state23Dependency.Name state21Dependency.Name state22Dependency.Name state23Dependency.Name)
            None [] []

        // Missing dependencies.

        "2 bit full adder missing dependencies",
        ("2-bit-adder", twoBitAdderState, [], []),
        makeError
            "Could not resolve dependency: \"full-adder\". Make sure a dependency with such name exists in the current project."
            (Some "2-bit-adder") [] []

        "2 bit full adder missing half adder dependency",
        ("2-bit-adder", twoBitAdderState, [fullAdderDependency], []),
        makeError
            "Could not resolve dependency: \"half-adder\". Make sure a dependency with such name exists in the current project."
            (Some "full-adder") [] []

        "2 bit full adder missing full adder dependency",
        ("2-bit-adder", twoBitAdderState, [halfAdderDependency], []),
        makeError
            "Could not resolve dependency: \"full-adder\". Make sure a dependency with such name exists in the current project."
            (Some "2-bit-adder") [] []
    ]

// Outputs fot the 2bit adder test.

let private zero = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [Zero]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [Zero]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [Zero]
    ]
let private one = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [Zero]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [One]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [Zero]
    ]
let private two = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [One]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [Zero]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [Zero]
    ]
let private three = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [One]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [One]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [Zero]
    ]
let private four = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [Zero]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [Zero]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [One]
    ]
let private five = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [Zero]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [One]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [One]
    ]
let private six = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [One]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [Zero]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [One]
    ]
let private seven = [
        (ComponentId "dbb1f55a-edf3-bde2-4c69-43a02560e17d", ComponentLabel "Sum1", 1), [One]
        (ComponentId "8f5bded5-f46d-722d-6108-03dda4236c01", ComponentLabel "Sum0", 1), [One]
        (ComponentId "7d948312-376d-1d4b-cf02-90872026be16", ComponentLabel "Cout", 1), [One]
    ]

let testCasesSimulatorOkWithDependencies : SimulatorTestCase list =
    createAllTestCases
        "Simple input-output dependency" "main"
        state16 [state3Dependency] [ComponentId "outer-input-node0"]
        [
            [(ComponentId "outer-output-node0", ComponentLabel "outer-output-node0-label", 1), [Zero]]
            [(ComponentId "outer-output-node0", ComponentLabel "outer-output-node0-label", 1), [One]]
        ]
    @
    createAllTestCases
        "Nested input-output dependency" "main"
        state17 [state16Dependency; state3Dependency] [ComponentId "outer-outer-input-node0"]
        [
            [(ComponentId "outer-outer-output-node0", ComponentLabel "outer-outer-output-node0-label", 1), [Zero]]
            [(ComponentId "outer-outer-output-node0", ComponentLabel "outer-outer-output-node0-label", 1), [One]]
        ]
    @
    createAllTestCases
        "Doubly nested input-output dependency" "main"
        state18 [state17Dependency; state16Dependency; state3Dependency] [ComponentId "outer-outer-outer-input-node0"]
        [
            [(ComponentId "outer-outer-outer-output-node0", ComponentLabel "outer-outer-outer-output-node0-label", 1), [Zero]]
            [(ComponentId "outer-outer-outer-output-node0", ComponentLabel "outer-outer-outer-output-node0-label", 1), [One]]
        ]
    @
    createAllTestCases
        "2 bit adder" "2-bit-adder"
        twoBitAdderState [fullAdderDependency; halfAdderDependency] [
            ComponentId "78795182-35c4-1c50-2190-6fc944a2adea" // Cin
            ComponentId "a63fe5a2-9f4d-e70f-131b-ed35d3f3a9e1" // B1
            ComponentId "69a6ad2a-af19-369f-0483-0e09e6841da3" // B0
            ComponentId "82a03f0b-ae31-b487-ed1b-335e235adeb7" // A1
            ComponentId "86372781-c2f4-09f2-406f-f385ee7a47a9" // A0
        ]
        [
            zero;one;two;three;
            one;two;three;four;
            two;three;four;five;
            three;four;five;six;
            one;two;three;four;
            two;three;four;five;
            three;four;five;six;
            four;five;six;seven
        ]

let private testCasesSimulatorBusesError : SimulatorTestCase list = [
    "Two inputs make a bus2, then Push input a to bus, then try to split into 2 single bits (fail)",
    ("main", stateBus11, [], []),
    makeError "Wrong wire width. Target port expects a 1 bit(s) signal, but source port produces a 2 bit(s) signal." None [] ["conn"]

    "A 4 bit input connected to a 3 bit output",
    ("main", stateBus14, [], []),
    makeError "Wrong wire width. Target port expects a 3 bit(s) signal, but source port produces a 4 bit(s) signal." None [] ["conn"]

    "A 3 bit input connected to a 4 bit output",
    ("main", stateBus15, [], []),
    makeError "Wrong wire width. Target port expects a 4 bit(s) signal, but source port produces a 3 bit(s) signal." None [] ["conn"]
]

let private testCasesSimulatorOkWithBuses : SimulatorTestCase list =
    createAllTestCases
        "Two inputs, packed into a bus, unpacked into two outputs" "main"
        stateBus10 [] [
            ComponentId "a91be585-2d3b-d872-be0f-b416c8eb03d2" // a
            ComponentId "9985ebc6-1cd5-8863-1341-1d543d236d38" // b
        ]
        (makeAllBitCombinations [
            (ComponentId "8a9392fc-493b-7e96-72ec-b6f5f11ded8a", ComponentLabel "a-out", 1)
            (ComponentId "dfcf6cff-fbac-e54f-7a9d-7059d17e3a0b", ComponentLabel "b-out", 1)
        ])
    @
    createAllTestCases
        "Four inputs, packed into a bus, unpacked into four outputs" "main"
        stateBus12 [] [
            ComponentId "76de964a-124b-5c16-6de1-6158626344ac" // a
            ComponentId "a91be585-2d3b-d872-be0f-b416c8eb03d2" // b
            ComponentId "9985ebc6-1cd5-8863-1341-1d543d236d38" // c
            ComponentId "9824ceb8-e999-8e48-9a56-7a4349e495b1" // d
        ]
        (makeAllBitCombinations [
            (ComponentId "59b45f9c-192c-98ce-da25-a94db45a5790", ComponentLabel "a-out", 1)
            (ComponentId "8a9392fc-493b-7e96-72ec-b6f5f11ded8a", ComponentLabel "b-out", 1)
            (ComponentId "dfcf6cff-fbac-e54f-7a9d-7059d17e3a0b", ComponentLabel "c-out", 1)
            (ComponentId "214620f0-51f6-59fe-1558-ed47fd2c680a", ComponentLabel "d-out", 1)
        ])
    @
    [
        "A 4 bit input connected to a four bit output (1)",
        ("main", stateBus13, [], [
            ComponentId "9bcba47e-deae-0b3f-2079-a1b124526b00", [One; One; One; One]
        ]),
        [
            (ComponentId "ad2ef0c3-537e-9d2e-0064-ac6b952e4b97", ComponentLabel "b", 4), [One; One; One; One]
        ] |> Ok

        "A 4 bit input connected to a four bit output (2)",
        ("main", stateBus13, [], [
            ComponentId "9bcba47e-deae-0b3f-2079-a1b124526b00", [Zero; One; Zero; One]
        ]),
        [
            (ComponentId "ad2ef0c3-537e-9d2e-0064-ac6b952e4b97", ComponentLabel "b", 4), [Zero; One; Zero; One]
        ] |> Ok

        "A 2 bit input split into 2 single bit outputs (1)",
        ("main", stateBus16, [], [
            ComponentId "c6f000db-310f-d8ad-ff5e-938d7c2aaa7c", [One; Zero]
        ]),
        [
            (ComponentId "60e2df66-bb8c-53f1-832d-e154c30cf9dd", ComponentLabel "b", 1), [One]
            (ComponentId "85e19389-c087-8b30-6c0a-02f7cc753695", ComponentLabel "c", 1), [Zero]
        ] |> Ok

        "A 2 bit input split into 2 single bit outputs (2)",
        ("main", stateBus16, [], [
            ComponentId "c6f000db-310f-d8ad-ff5e-938d7c2aaa7c", [Zero; One]
        ]),
        [
            (ComponentId "60e2df66-bb8c-53f1-832d-e154c30cf9dd", ComponentLabel "b", 1), [Zero]
            (ComponentId "85e19389-c087-8b30-6c0a-02f7cc753695", ComponentLabel "c", 1), [One]
        ] |> Ok

        "3 bit input merged with 4 bit input, then split in the same way",
        ("main", stateBus17, [], [
            ComponentId "6bcdc74a-9d71-3304-537d-1a17f02924eb", [One; Zero; One] // 3 bits.
            ComponentId "97c4b56d-4f8c-2b00-fb61-a08cdd01dd76", [Zero; One; Zero; One] // 4 bits.
        ]),
        [
            (ComponentId "1a6e1bb4-cfe0-77f9-a207-f409168ef210", ComponentLabel "out3", 3), [One; Zero; One]
            (ComponentId "d2676492-2302-24d9-52eb-6e69e7971339", ComponentLabel "out4", 4), [Zero; One; Zero; One]
        ] |> Ok
    ]

let testCasesSimulator =
    testCasesSimulatorPortError @
    testCasesSimulatorCycleError @
    testCasesSimulatorDuplicatIOError @
    testCasesSimulatorOkNoDependencies @
    testCasesSimulatorDependencyError @
    testCasesSimulatorOkWithDependencies @
    testCasesSimulatorBusesError @
    testCasesSimulatorOkWithBuses
