"""Basic tests of the action graph"""
import omni.client
import omni.graph.core as og
import omni.graph.core.tests as ogts
import omni.kit.test
import omni.usd
from pxr import Gf, Sdf


# ======================================================================
class TestActionGraphNodes(ogts.OmniGraphTestCase):
    """Tests action graph node functionality"""

    TEST_GRAPH_PATH = "/World/TestGraph"

    async def setUp(self):
        """Set up  test environment, to be torn down when done"""
        await super().setUp()
        og.Controller.edit({"graph_path": self.TEST_GRAPH_PATH, "evaluator_name": "execution"})

    # ----------------------------------------------------------------------
    async def test_basic(self):
        """exercise a basic network of execution nodes"""
        controller = og.Controller()
        keys = og.Controller.Keys

        # Test that an execution connection is correctly triggering the downstream node once per evaluation
        (graph, (_, flip_flop_node), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [("OnTick", "omni.graph.action.OnTick"), ("FlipFlop", "omni.graph.action.FlipFlop")],
                keys.SET_VALUES: [("OnTick.inputs:onlyPlayback", False)],
                keys.CONNECT: ("OnTick.outputs:tick", "FlipFlop.inputs:execIn"),
            },
        )

        outa = controller.attribute("outputs:a", flip_flop_node)
        outb = controller.attribute("outputs:b", flip_flop_node)
        outisa = controller.attribute("outputs:isA", flip_flop_node)

        # first eval, 'a'
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), og.ExecutionAttributeState.ENABLED)
        self.assertEqual(og.Controller.get(outb), og.ExecutionAttributeState.DISABLED)
        self.assertTrue(og.Controller.get(outisa))

        # second eval 'b'
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), og.ExecutionAttributeState.DISABLED)
        self.assertEqual(og.Controller.get(outb), og.ExecutionAttributeState.ENABLED)
        self.assertFalse(og.Controller.get(outisa))

        # third eval 'a'
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), og.ExecutionAttributeState.ENABLED)
        self.assertEqual(og.Controller.get(outb), og.ExecutionAttributeState.DISABLED)
        self.assertTrue(og.Controller.get(outisa))

        # Test that non-usd-backed node has execution attribute type correct
        _, on_tick_no_usd = og.cmds.CreateNode(
            graph=graph,
            node_path=f"{self.TEST_GRAPH_PATH}/OnTickNoUSD",
            node_type="omni.graph.action.OnTick",
            create_usd=False,
        )
        og.cmds.CreateNode(
            graph=graph,
            node_path=f"{self.TEST_GRAPH_PATH}/FlipFlopNoUSD",
            node_type="omni.graph.action.FlipFlop",
            create_usd=False,
        )
        controller.attribute("inputs:onlyPlayback", on_tick_no_usd, graph).set(False)

        og.cmds.ConnectAttrs(
            src_attr=f"{self.TEST_GRAPH_PATH}/OnTickNoUSD.outputs:tick",
            dest_attr=f"{self.TEST_GRAPH_PATH}/FlipFlopNoUSD.inputs:execIn",
            modify_usd=False,
        )

        outa = controller.attribute("outputs:a", f"{self.TEST_GRAPH_PATH}/FlipFlopNoUSD")
        outb = controller.attribute("outputs:b", f"{self.TEST_GRAPH_PATH}/FlipFlopNoUSD")
        outisa = controller.attribute("outputs:isA", f"{self.TEST_GRAPH_PATH}/FlipFlopNoUSD")

        # first eval, 'a'
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), og.ExecutionAttributeState.ENABLED)
        self.assertEqual(og.Controller.get(outb), og.ExecutionAttributeState.DISABLED)
        self.assertTrue(og.Controller.get(outisa))

        # second eval 'b'
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), og.ExecutionAttributeState.DISABLED)
        self.assertEqual(og.Controller.get(outb), og.ExecutionAttributeState.ENABLED)
        self.assertFalse(og.Controller.get(outisa))

        # third eval 'a'
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), og.ExecutionAttributeState.ENABLED)
        self.assertEqual(og.Controller.get(outb), og.ExecutionAttributeState.DISABLED)
        self.assertTrue(og.Controller.get(outisa))

    # ----------------------------------------------------------------------
    async def test_notice(self):
        """Tests the OnObjectChange node"""
        usd_context = omni.usd.get_context()
        stage = usd_context.get_stage()

        controller = og.Controller()
        keys = og.Controller.Keys

        cube = ogts.create_cube(stage, "Cube", (0.6, 0.4, 0.0))
        attr_position = cube.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Float3, False)
        attr_rotation = cube.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Float3, False)

        (graph, (on_object_change_node, flip_flop_node), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnObjectChange", "omni.graph.action.OnObjectChange"),
                    ("FlipFlop", "omni.graph.action.FlipFlop"),
                ],
                keys.SET_VALUES: [
                    ("OnObjectChange.inputs:onlyPlayback", False),
                    ("OnObjectChange.inputs:path", attr_position.GetPath().pathString),
                ],
                keys.CONNECT: ("OnObjectChange.outputs:changed", "FlipFlop.inputs:execIn"),
            },
        )

        outa = controller.attribute("outputs:a", flip_flop_node)

        # check current flipflop state, and that it isn't changing until we move the cube
        await controller.evaluate(graph)
        ff_a_state_1 = og.Controller.get(outa)
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), ff_a_state_1)

        # Try changing a different attr - should not trigger
        attr_rotation.Set(Gf.Vec3f(180.0, 0.0, 0.0))
        await controller.evaluate(graph)
        self.assertEqual(og.Controller.get(outa), ff_a_state_1)

        # Now change the translate - should trigger
        attr_position.Set(Gf.Vec3f(1.0, 0.0, 0.0))
        await controller.evaluate(graph)

        ff_a_state_2 = og.Controller.get(outa)
        self.assertEqual(ff_a_state_2, 1 if not ff_a_state_1 else 0)
        property_path = og.Controller.get(controller.attribute("outputs:propertyName", on_object_change_node))
        self.assertEqual(property_path, attr_position.GetName())

        # Look at prim itself
        controller.edit(graph, {keys.SET_VALUES: ("OnObjectChange.inputs:path", cube.GetPath().pathString)})
        await controller.evaluate(graph)

        # Now change the rotate - should trigger
        attr_rotation.Set(Gf.Vec3f(245.0, 0.0, 0.0))
        await controller.evaluate(graph)

        ff_a_state_1 = og.Controller.get(outa)
        self.assertEqual(ff_a_state_1, 1 if not ff_a_state_2 else 0)
        property_path = og.Controller.get(controller.attribute("outputs:propertyName", on_object_change_node))
        self.assertEqual(property_path, attr_rotation.GetName())

        # Now use a prim rel instead of the path input
        inputs_prim = stage.GetPrimAtPath(on_object_change_node.get_prim_path()).GetRelationship("inputs:prim")
        inputs_prim.AddTarget(cube.GetPath())
        controller.edit(graph, {keys.SET_VALUES: ("OnObjectChange.inputs:path", "")})
        # compute once to set up the inputs
        await controller.evaluate(graph)
        # change rotate - should trigger
        attr_rotation.Set(Gf.Vec3f(0.0, 0.0, 0.0))
        await controller.evaluate(graph)

        ff_a_state_2 = og.Controller.get(outa)
        self.assertEqual(ff_a_state_2, 1 if not ff_a_state_1 else 0)
        property_path = og.Controller.get(controller.attribute("outputs:propertyName", on_object_change_node))
        self.assertEqual(property_path, attr_rotation.GetName())

    # ----------------------------------------------------------------------
    async def test_onplaybacktick(self):
        """Test OnPlaybackTick and OnTick node"""
        controller = og.Controller()
        keys = og.Controller.Keys
        app = omni.kit.app.get_app()
        timeline = omni.timeline.get_timeline_interface()

        timeline.set_start_time(1.0)
        timeline.set_end_time(10.0)
        fps = 24.0
        timeline.set_time_codes_per_second(fps)

        (graph, (on_p_tick_node, on_tick_node, _), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnPTick", "omni.graph.action.OnPlaybackTick"),
                    ("OnTick", "omni.graph.action.OnTick"),
                    ("FlipFlop", "omni.graph.action.FlipFlop"),
                ],
                keys.CONNECT: [
                    ("OnPTick.outputs:tick", "FlipFlop.inputs:execIn"),
                    ("OnTick.outputs:tick", "FlipFlop.inputs:execIn"),
                ],
                keys.SET_VALUES: ("OnTick.inputs:onlyPlayback", True),
            },
        )
        await controller.evaluate(graph)

        tick_time = controller.attribute("outputs:time", on_p_tick_node)
        tick_frame = controller.attribute("outputs:frame", on_p_tick_node)
        ontick_time = controller.attribute("outputs:time", on_tick_node)
        ontick_frame = controller.attribute("outputs:frame", on_tick_node)
        # Check that the OnPlaybackTick node doesn't trigger when playback is not active
        self.assertEqual(og.Controller.get(tick_time), 0)
        self.assertEqual(og.Controller.get(tick_frame), 0)
        self.assertEqual(og.Controller.get(ontick_time), 0)
        self.assertEqual(og.Controller.get(ontick_frame), 0)

        # Check that the OnPlaybackTick node does trigger when playback is active, and the values are correct
        timeline.play()
        await app.next_update_async()
        await controller.evaluate(graph)
        t0 = og.Controller.get(tick_time)
        f0 = og.Controller.get(tick_frame)
        self.assertAlmostEqual(t0, timeline.get_current_time(), places=5)
        self.assertAlmostEqual(f0, t0 * fps, places=5)
        t0 = og.Controller.get(ontick_time)
        f0 = og.Controller.get(ontick_frame)
        self.assertAlmostEqual(t0, timeline.get_current_time(), places=5)
        self.assertAlmostEqual(f0, t0 * fps, places=5)

    # ----------------------------------------------------------------------
    async def test_non_action_subgraph(self):
        """Tests subgraphs in action graph"""
        controller = og.Controller()
        keys = og.Controller.Keys

        # Test that a push subgraph is ticked by the top level Action graph

        subgraph_path = self.TEST_GRAPH_PATH + "/push_sub"
        graph = omni.graph.core.get_current_graph()
        graph.create_subgraph(subgraph_path, evaluator="push")
        subgraph = graph.get_subgraph(subgraph_path)
        self.assertTrue(subgraph.is_valid())

        sub_ff_node = self.TEST_GRAPH_PATH + "/push_sub/FlipFlop"

        controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnTick", "omni.graph.action.OnTick"),
                    ("FlipFlop", "omni.graph.action.FlipFlop"),
                ],
                keys.CONNECT: ("OnTick.outputs:tick", "FlipFlop.inputs:execIn"),
            },
        )
        await og.Controller.evaluate()

        controller.edit(subgraph, {keys.CREATE_NODES: (sub_ff_node, "omni.graph.action.FlipFlop")})

        sub_flipflop_node = subgraph.get_node(sub_ff_node)
        self.assertTrue(sub_flipflop_node.is_valid())

        ffstate0 = og.Controller.get(controller.attribute("outputs:isA", sub_ff_node))
        await og.Controller.evaluate()
        ffstate1 = og.Controller.get(controller.attribute("outputs:isA", sub_ff_node))
        self.assertNotEqual(ffstate0, ffstate1)

    # ----------------------------------------------------------------------
    async def test_custom_events(self):
        """Test SendCustomEvent and OnCustomEvent node"""
        controller = og.Controller()
        keys = og.Controller.Keys

        await omni.kit.app.get_app().next_update_async()

        (_, (_, _, event1_node, counter1_node, event2_node, counter2_node), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnImpulse", "omni.graph.action.OnImpulseEvent"),
                    ("Send", "omni.graph.action.SendCustomEvent"),
                    ("OnCustomEvent1", "omni.graph.action.OnCustomEvent"),
                    ("Counter1", "omni.graph.action.Counter"),
                    ("OnCustomEvent2", "omni.graph.action.OnCustomEvent"),
                    ("Counter2", "omni.graph.action.Counter"),
                ],
                keys.CONNECT: [
                    ("OnImpulse.outputs:execOut", "Send.inputs:execIn"),
                    ("OnCustomEvent1.outputs:execOut", "Counter1.inputs:execIn"),
                    ("OnCustomEvent2.outputs:execOut", "Counter2.inputs:execIn"),
                ],
                keys.SET_VALUES: [
                    ("OnCustomEvent1.inputs:onlyPlayback", False),
                    ("OnCustomEvent2.inputs:onlyPlayback", False),
                    ("OnImpulse.inputs:onlyPlayback", False),
                    ("Send.inputs:eventName", "foo"),
                    ("Send.inputs:path", "Test Path"),
                    ("OnCustomEvent1.inputs:eventName", "foo"),
                    ("OnCustomEvent2.inputs:eventName", "foo"),
                ],
            },
        )

        counter1_controller = og.Controller(og.Controller.attribute("outputs:count", counter1_node))
        counter2_controller = og.Controller(og.Controller.attribute("outputs:count", counter2_node))
        event1_controller = og.Controller(og.Controller.attribute("outputs:path", event1_node))
        event2_controller = og.Controller(og.Controller.attribute("outputs:path", event2_node))

        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(counter1_controller.get(), 0)
        # trigger graph once, this will queue up the event for the next evaluation
        controller.edit(self.TEST_GRAPH_PATH, {keys.SET_VALUES: ("OnImpulse.state:enableImpulse", True)})
        # Note that if this is a push subscription, the receivers will run this frame instead of next
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(counter1_controller.get(), 0)
        # This evaluation should trigger the receivers
        await omni.kit.app.get_app().next_update_async()
        # Verify that events were received
        self.assertEqual(counter1_controller.get(), 1)
        self.assertEqual(event1_controller.get(), "Test Path")
        self.assertEqual(counter2_controller.get(), 1)
        self.assertEqual(event2_controller.get(), "Test Path")
        # Verify the contents of the associated bundle
        # FIXME: Authored bundle is always empty?
        # bundle_contents = og.BundleContents(graph.get_default_graph_context(), event1_node, "outputs:bundle", True)
        # self.assertEqual(1, bundle_contents.size)

        # Modify the event name one receiver and sender and ensure it still works
        controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.SET_VALUES: [("Send.inputs:eventName", "bar"), ("OnImpulse.state:enableImpulse", True)],
            },
        )
        await omni.kit.app.get_app().next_update_async()
        await omni.kit.app.get_app().next_update_async()
        # We changed the sender event name, so counter should _not_ have triggered again
        self.assertEqual(counter1_controller.get(), 1)
        # Change the receiver name to match
        controller.edit(self.TEST_GRAPH_PATH, {keys.SET_VALUES: ("OnCustomEvent1.inputs:eventName", "bar")})
        await omni.kit.app.get_app().next_update_async()
        # trigger send again and verify we get it (1 frame lag for pop)
        controller.edit(self.TEST_GRAPH_PATH, {keys.SET_VALUES: ("OnImpulse.state:enableImpulse", True)})
        await omni.kit.app.get_app().next_update_async()
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(counter1_controller.get(), 2)

    # ----------------------------------------------------------------------
    async def test_request_driven_node(self):
        """Test that RequestDriven nodes are computed as expected"""
        controller = og.Controller()
        keys = og.Controller.Keys

        (graph, (_, counter_node), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnImpulse", "omni.graph.action.OnImpulseEvent"),
                    ("Counter", "omni.graph.action.Counter"),
                ],
                keys.SET_VALUES: [("OnImpulse.inputs:onlyPlayback", False)],
                keys.CONNECT: ("OnImpulse.outputs:execOut", "Counter.inputs:execIn"),
            },
        )
        # After several updates, there should have been no compute calls
        await controller.evaluate(graph)
        await controller.evaluate(graph)
        await controller.evaluate(graph)
        counter_controller = og.Controller(og.Controller.attribute("outputs:count", counter_node))
        self.assertEqual(counter_controller.get(), 0)

        # change OnImpulse state attrib. The node should now request compute
        controller.edit(self.TEST_GRAPH_PATH, {keys.SET_VALUES: ("OnImpulse.state:enableImpulse", True)})
        await controller.evaluate(graph)
        self.assertEqual(counter_controller.get(), 1)
        # more updates should not result in more computes
        await controller.evaluate(graph)
        await controller.evaluate(graph)
        await controller.evaluate(graph)
        self.assertEqual(counter_controller.get(), 1)

    # ----------------------------------------------------------------------
    async def test_stage_events(self):
        """Test OnStageEvent"""
        controller = og.Controller()
        keys = og.Controller.Keys

        (_, (_, _, _, counter_node, counter2_node, counter3_node), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnStageEvent", "omni.graph.action.OnStageEvent"),
                    ("OnStageEvent2", "omni.graph.action.OnStageEvent"),
                    ("OnStageEvent3", "omni.graph.action.OnStageEvent"),
                    ("Counter", "omni.graph.action.Counter"),
                    ("Counter2", "omni.graph.action.Counter"),
                    ("Counter3", "omni.graph.action.Counter"),
                ],
                keys.CONNECT: [
                    ("OnStageEvent.outputs:execOut", "Counter.inputs:execIn"),
                    ("OnStageEvent2.outputs:execOut", "Counter2.inputs:execIn"),
                    ("OnStageEvent3.outputs:execOut", "Counter3.inputs:execIn"),
                ],
                keys.SET_VALUES: [
                    ("OnStageEvent.inputs:eventName", "Selection Changed"),
                    ("OnStageEvent.inputs:onlyPlayback", False),
                    ("OnStageEvent2.inputs:eventName", "Animation Stop Play"),
                    ("OnStageEvent2.inputs:onlyPlayback", True),
                    ("OnStageEvent3.inputs:eventName", "Animation Start Play"),
                    ("OnStageEvent3.inputs:onlyPlayback", True),
                ],
            },
        )
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter_node)), 0)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter2_node)), 0)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter3_node)), 0)

        selection = omni.usd.get_context().get_selection()
        selection.set_selected_prim_paths([self.TEST_GRAPH_PATH + "/OnStageEvent"], False)
        # 1 frame delay on the pop, 1 frame delay on the compute
        await omni.kit.app.get_app().next_update_async()
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter_node)), 1)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter2_node)), 0)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter3_node)), 0)

        # Verify that start/stop events work when only-plackback is true
        timeline = omni.timeline.get_timeline_interface()

        timeline.set_start_time(1.0)
        timeline.set_end_time(10.0)
        timeline.play()
        await omni.kit.app.get_app().next_update_async()
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter2_node)), 0)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter3_node)), 1)
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter2_node)), 0)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter3_node)), 1)
        timeline.stop()
        await omni.kit.app.get_app().next_update_async()
        await omni.kit.app.get_app().next_update_async()
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter2_node)), 1)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter3_node)), 1)
        await controller.evaluate()
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter2_node)), 1)
        self.assertEqual(og.Controller.get(controller.attribute("outputs:count", counter3_node)), 1)

    # -----------------------------------------------------------------------
    async def test_add_prim_relationship(self):
        """Test AddPrimRelationship"""
        controller = og.Controller()
        # Check that we can add relationship to a prim and get that relationship
        #
        # +---------+   +---------------------+
        # | ONTICK  +-->| AddPrimRelationship +
        # +---------+   +---------------------+
        #
        usd_context = omni.usd.get_context()
        stage = usd_context.get_stage()
        controller = og.Controller()
        keys = og.Controller.Keys

        controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnTick", "omni.graph.action.OnTick"),
                    ("AddRel", "omni.graph.action.AddPrimRelationship"),
                ],
                keys.CREATE_PRIMS: [
                    ("/Test", {}),
                    ("/Target", {}),
                ],
                keys.CONNECT: [("OnTick.outputs:tick", "AddRel.inputs:execIn")],
                keys.SET_VALUES: [
                    ("OnTick.inputs:onlyPlayback", False),
                    ("AddRel.inputs:path", "/Test"),
                    ("AddRel.inputs:name", "rel"),
                    ("AddRel.inputs:target", "/Target"),
                ],
            },
        )

        prim = stage.GetPrimAtPath("/Test")

        await controller.evaluate()

        rel = prim.GetRelationship("rel")
        targets = rel.GetTargets()
        self.assertEqual(len(targets), 1)
        self.assertTrue(str(targets[0]) == "/Target")

    # ----------------------------------------------------------------------
    async def test_switch(self):
        """Test the Switch nodes"""
        controller = og.Controller()
        keys = og.Controller.Keys

        # Test that an execution connection is correctly triggering the downstream node once per evaluation
        (graph, (_, switch_node), _, _,) = controller.edit(
            self.TEST_GRAPH_PATH,
            {
                keys.CREATE_NODES: [
                    ("OnTick", "omni.graph.action.OnTick"),
                    ("Switch", "omni.graph.action.SwitchToken"),
                ],
                keys.SET_VALUES: [
                    ("OnTick.inputs:onlyPlayback", False),
                ],
                keys.CONNECT: [
                    ("OnTick.outputs:tick", "Switch.inputs:execIn"),
                ],
            },
        )

        def add_input(index: int):
            controller.create_attribute(
                switch_node,
                f"inputs:branch{index:02}",
                "token",
                og.AttributePortType.ATTRIBUTE_PORT_TYPE_INPUT,
            )
            controller.create_attribute(
                switch_node,
                f"outputs:output{index:02}",
                "execution",
                og.AttributePortType.ATTRIBUTE_PORT_TYPE_OUTPUT,
            )
            og.Controller.set(controller.attribute(f"inputs:branch{index:02}", switch_node), f"{index:02}")

        for index in range(5):
            add_input(index)

        for index in range(5):
            og.Controller.set(controller.attribute("inputs:value", switch_node), f"{index:02}")
            await controller.evaluate(graph)
            for index2 in range(5):
                expected_val = (
                    og.ExecutionAttributeState.DISABLED if index2 != index else og.ExecutionAttributeState.ENABLED
                )
                self.assertEqual(
                    og.Controller.get(controller.attribute(f"outputs:output{index2:02}", switch_node)), expected_val
                )
