# Copyright 2025 Red Hat, Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from neutron.agent.ovn.extensions.bgp import commands
from neutron.agent.ovn.extensions.bgp import exceptions
from neutron.common.ovn import constants as ovn_const
from neutron.services.bgp import constants
from neutron.services.bgp import helpers
from neutron.tests.common import net_helpers
from neutron.tests.functional.agent.ovn.extensions import bgp as test_bgp
from neutron.tests.functional import base as func_base
from neutron.tests.functional.services import bgp


_get_unique_name = func_base.get_unique_name


class SetChassisBgpBridgesCommandTestCase(bgp.BaseBgpSbIdlTestCase):
    def _get_chassis_bgp_bridges(self, chassis_name):
        chassis = self.sb_api.lookup('Chassis_Private', chassis_name)
        bridges_str = chassis.external_ids.get(
            constants.CHASSIS_BGP_BRIDGES_EXT_ID_KEY, '')
        if bridges_str:
            return bridges_str.split(',')
        return []

    def test_set_bridges_on_chassis_without_existing_bridges(self):
        chassis = self.add_fake_chassis(_get_unique_name("chassis"), '1.1.1.1')
        bridge_name = _get_unique_name("bridge")

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertFalse(bridges)

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name, [bridge_name]).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([bridge_name], bridges)

    def test_set_bridges_replaces_existing_bridges(self):
        existing_bridge = _get_unique_name("existing_bridge")
        chassis = self.add_fake_chassis(
            _get_unique_name("chassis"), '1.1.1.2',
            external_ids={
                constants.CHASSIS_BGP_BRIDGES_EXT_ID_KEY: existing_bridge
            })
        new_bridge = _get_unique_name("new_bridge")

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([existing_bridge], bridges)

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name, [new_bridge]).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([new_bridge], bridges)

    def test_set_bridges_is_idempotent(self):
        bridge_name = _get_unique_name("bridge")
        chassis = self.add_fake_chassis(
            _get_unique_name("chassis"), '1.1.1.3',
            external_ids={
                constants.CHASSIS_BGP_BRIDGES_EXT_ID_KEY: bridge_name
            })

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([bridge_name], bridges)

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name, [bridge_name]).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([bridge_name], bridges)

    def test_set_bridges_replaces_multiple_existing(self):
        existing_bridge1 = _get_unique_name("existing_bridge1")
        existing_bridge2 = _get_unique_name("existing_bridge2")
        existing_bridges_str = f"{existing_bridge1},{existing_bridge2}"
        chassis = self.add_fake_chassis(
            _get_unique_name("chassis"), '1.1.1.4',
            external_ids={
                constants.CHASSIS_BGP_BRIDGES_EXT_ID_KEY: existing_bridges_str
            })
        new_bridge = _get_unique_name("new_bridge")

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertCountEqual([existing_bridge1, existing_bridge2], bridges)

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name, [new_bridge]).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([new_bridge], bridges)

    def test_set_multiple_bridges_at_once(self):
        chassis = self.add_fake_chassis(_get_unique_name("chassis"), '1.1.1.5')
        bridge1 = _get_unique_name("bridge1")
        bridge2 = _get_unique_name("bridge2")
        bridge3 = _get_unique_name("bridge3")

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name,
            [bridge1, bridge2, bridge3]).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertCountEqual([bridge1, bridge2, bridge3], bridges)

    def test_set_bridges_preserves_other_external_ids(self):
        other_key = 'other-external-id'
        other_value = 'some-value'
        chassis = self.add_fake_chassis(
            _get_unique_name("chassis"), '1.1.1.6',
            external_ids={other_key: other_value})
        bridge_name = _get_unique_name("bridge")

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name, [bridge_name]).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([bridge_name], bridges)

        chassis = self.sb_api.lookup('Chassis_Private', chassis.name)
        self.assertEqual(other_value, chassis.external_ids.get(other_key))

    def test_set_empty_bridges_clears_existing(self):
        existing_bridge = _get_unique_name("existing_bridge")
        chassis = self.add_fake_chassis(
            _get_unique_name("chassis"), '1.1.1.7',
            external_ids={
                constants.CHASSIS_BGP_BRIDGES_EXT_ID_KEY: existing_bridge
            })

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertEqual([existing_bridge], bridges)

        commands.SetChassisBgpBridgesCommand(
            self.sb_api, chassis.name, []).execute(check_error=True)

        bridges = self._get_chassis_bgp_bridges(chassis.name)
        self.assertFalse(bridges)


class GetPatchPortsFromBridgeCommandTestCase(bgp.BaseBgpIDLTestCase):
    schemas = ['Open_vSwitch']

    def setUp(self):
        super().setUp()
        self.bridge_name = test_bgp.unique_bridge_name('br')
        self.peer_bridge_name = test_bgp.unique_bridge_name('peer')
        self.ovs_api.add_br(self.bridge_name).execute(check_error=True)
        self.ovs_api.add_br(self.peer_bridge_name).execute(check_error=True)

    def _add_patch_port(self, port_name, peer_name):
        with self.ovs_api.transaction(check_error=True) as txn:
            txn.add(self.ovs_api.add_port(self.bridge_name, port_name))
            txn.add(self.ovs_api.add_port(self.peer_bridge_name, peer_name))
            txn.add(self.ovs_api.db_set(
                'Interface', port_name, type='patch',
                options={'peer': peer_name}))
            txn.add(self.ovs_api.db_set(
                'Interface', peer_name, type='patch',
                options={'peer': port_name}))

    def _execute_command(self):
        return commands.GetPatchPortsFromBridgeCommand(
            self.ovs_api, self.bridge_name).execute(check_error=True)

    def test_returns_patch_ports_on_bridge(self):
        self._add_patch_port('patch-a', 'peer-a')
        self._add_patch_port('patch-b', 'peer-b')

        result = self._execute_command()

        names = {iface.name for iface in result}
        self.assertEqual({'patch-a', 'patch-b'}, names)

    def test_does_not_return_ports_from_other_bridge(self):
        self._add_patch_port('patch-a', 'peer-a')

        result = self._execute_command()

        names = {iface.name for iface in result}
        self.assertNotIn('peer-a', names)

    def test_does_not_return_non_patch_ports(self):
        fake_nic = self.useFixture(net_helpers.VethFixture()).ports[0]
        self.ovs_api.add_port(
            self.bridge_name, fake_nic.name).execute(check_error=True)
        self._add_patch_port('patch-a', 'peer-a')

        result = self._execute_command()

        names = {iface.name for iface in result}
        self.assertIn('patch-a', names)
        self.assertNotIn(fake_nic.name, names)

    def test_empty_bridge_returns_empty_list(self):
        result = self._execute_command()
        self.assertEqual([], result)

    def test_returned_interfaces_have_ofport(self):
        self._add_patch_port('patch-a', 'peer-a')

        result = self._execute_command()

        for iface in result:
            self.assertTrue(iface.ofport)


class GetInterconnectLrpMacCommandTestCase(bgp.BaseBgpNbIdlTestCase):

    def setUp(self):
        super().setUp()
        self.router_name = constants.MAIN_ROUTER_NAME
        self.switch_name = _get_unique_name('ls')
        ic_switch_name = _get_unique_name('bgp-ls-interconnect')
        self.lrp_name = helpers.get_lrp_name(self.router_name, ic_switch_name)
        self.lrp_mac = 'aa:bb:cc:dd:ee:ff'
        self.localnet_lsp_name = helpers.get_lsp_localnet_name(ic_switch_name)

        with self.nb_api.transaction(check_error=True) as txn:
            txn.add(self.nb_api.lr_add(self.router_name))
            txn.add(self.nb_api.ls_add(self.switch_name))
            txn.add(self.nb_api.lrp_add(
                self.router_name, self.lrp_name,
                mac=self.lrp_mac, networks=[]))
            txn.add(self.nb_api.lsp_add(
                self.switch_name, _get_unique_name('rtr-lsp'),
                type=ovn_const.LSP_TYPE_ROUTER,
                addresses=['router'],
                options={'router-port': self.lrp_name}))
            txn.add(self.nb_api.lsp_add(
                self.switch_name, self.localnet_lsp_name,
                type=ovn_const.LSP_TYPE_LOCALNET,
                addresses=['unknown'],
                options={'network_name': 'test-net'}))

    def _execute_command(self, lsp_name=None):
        return commands.GetInterconnectLrpMacCommand(
            self.nb_api, lsp_name or self.localnet_lsp_name
        ).execute(check_error=True)

    def test_returns_lrp_mac(self):
        result = self._execute_command()
        self.assertEqual(self.lrp_mac, result)

    def test_nonexistent_lsp_raises(self):
        self.assertRaises(
            exceptions.InterconnectLrpMacNotFound,
            self._execute_command, 'no-such-lsp')

    def test_switch_without_router_port_raises(self):
        ls_name = _get_unique_name('ls')
        localnet_name = _get_unique_name('localnet')
        with self.nb_api.transaction(check_error=True) as txn:
            txn.add(self.nb_api.ls_add(ls_name))
            txn.add(self.nb_api.lsp_add(
                ls_name, localnet_name,
                type=ovn_const.LSP_TYPE_LOCALNET,
                addresses=['unknown'],
                options={'network_name': 'net'}))

        self.assertRaises(
            exceptions.InterconnectLrpMacNotFound,
            self._execute_command, localnet_name)
