# (c) cavaliba.com - tests / app_data.ipam.get_subnet_children / build_subnet_forest

from django.template.loader import render_to_string
from django.test import TestCase, override_settings
from django.urls import reverse

import app_home.cache as cache
from app_data.data import Instance
from app_data.ipam import _parse_cidr_fast, build_subnet_forest, get_subnet_children
from app_data.models import DataInstance
from tests import helper


class ParseCidrFastTest(TestCase):
    def test_valid_cidr(self):
        self.assertEqual(_parse_cidr_fast("10.1.2.0/24"), (167838208, 167838463, 24))

    def test_valid_slash32(self):
        start, end, prefixlen = _parse_cidr_fast("10.1.2.3/32")
        self.assertEqual(start, end)
        self.assertEqual(prefixlen, 32)

    def test_invalid_missing_mask(self):
        self.assertIsNone(_parse_cidr_fast("10.1.2.0"))

    def test_invalid_out_of_range_octet(self):
        self.assertIsNone(_parse_cidr_fast("10.1.2.999/24"))

    def test_invalid_out_of_range_prefix(self):
        self.assertIsNone(_parse_cidr_fast("10.1.2.0/33"))

    def test_invalid_not_a_cidr(self):
        self.assertIsNone(_parse_cidr_fast("not-a-cidr"))


class IpamSubnetChildrenTest(TestCase):
    def setUp(self):
        cache.clear()
        # TEST-NET-3 (RFC 5737) - guaranteed not to overlap the builtin
        # 10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12 seeds (220-ipam.yml)
        helper.add_instance(classname="ipam_subnet", keyname="203.0.0.0/16")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.1.0/24")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.2.0/24")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.3.0/25")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.19.128/30")
        # nested two levels deep under 203.0.1.0/24 - must NOT show up as a
        # direct child of 203.0.0.0/16
        helper.add_instance(classname="ipam_subnet", keyname="203.0.1.128/25")

    def test_returns_only_first_level_children(self):
        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.0.0/16")
        children = get_subnet_children(parent)

        keynames = [c["keyname"] for c in children]
        # numeric order (by CIDR start address), not keyname string order -
        # "203.0.19.128/30" would sort before "203.0.2.0/24" as a string
        self.assertEqual(
            keynames,
            ["203.0.1.0/24", "203.0.2.0/24", "203.0.3.0/25", "203.0.19.128/30"],
        )

    def test_grandchild_excluded_but_reachable_from_its_own_parent(self):
        mid = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.1.0/24")
        children = get_subnet_children(mid)
        self.assertEqual([c["keyname"] for c in children], ["203.0.1.128/25"])

    def test_leaf_subnet_has_no_children(self):
        leaf = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.3.0/25")
        self.assertEqual(get_subnet_children(leaf), [])

    def test_displayname_falls_back_to_keyname(self):
        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.0.0/16")
        children = get_subnet_children(parent)
        entry = next(c for c in children if c["keyname"] == "203.0.2.0/24")
        self.assertEqual(entry["displayname"], "203.0.2.0/24")

    def test_forest_is_cached_across_calls(self):
        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.0.0/16")
        get_subnet_children(parent)
        self.assertIsNotNone(cache.cache2_ipam_subnet_forest.get("forest"))

    def test_creating_new_subnet_invalidates_forest(self):
        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.0.0/16")
        before = [c["keyname"] for c in get_subnet_children(parent)]
        self.assertNotIn("203.0.4.0/24", before)

        helper.add_instance(classname="ipam_subnet", keyname="203.0.4.0/24")

        after = [c["keyname"] for c in get_subnet_children(parent)]
        self.assertIn("203.0.4.0/24", after)

    def test_deleting_subnet_invalidates_forest(self):
        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.0.0/16")
        before = [c["keyname"] for c in get_subnet_children(parent)]
        self.assertIn("203.0.2.0/24", before)

        target = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.2.0/24")
        target.delete()

        after = [c["keyname"] for c in get_subnet_children(parent)]
        self.assertNotIn("203.0.2.0/24", after)

    def test_inserting_intermediate_subnet_demotes_former_direct_child(self):
        """A new subnet inserted between 203.0.0.0/16 and 203.0.1.0/24 should
        make 203.0.1.0/24 a grandchild, not a direct child - the whole forest
        must be rebuilt, not just the new subnet's own entry."""
        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.0.0/16")
        before = [c["keyname"] for c in get_subnet_children(parent)]
        self.assertIn("203.0.1.0/24", before)

        helper.add_instance(classname="ipam_subnet", keyname="203.0.0.0/17")

        after = [c["keyname"] for c in get_subnet_children(parent)]
        self.assertNotIn("203.0.1.0/24", after)
        self.assertIn("203.0.0.0/17", after)


class BuildSubnetForestMalformedDataTest(TestCase):
    def setUp(self):
        cache.clear()

    def test_malformed_keyname_skipped_without_crashing(self):
        # bypass Instance/Schema validation entirely - simulates pre-existing
        # corrupted data (or a bug elsewhere), not something normal writes
        # through merge_import()/is_valid() can produce anymore
        DataInstance.objects.create(classname="ipam_subnet", keyname="not-a-cidr")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.5.0/24")

        forest = build_subnet_forest()

        self.assertNotIn("not-a-cidr", forest)
        for children in forest.values():
            self.assertNotIn("not-a-cidr", children)


class ViewsIpamSubnetChildrenHookTest(TestCase):
    def setUp(self):
        cache.clear()

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_children_rendered_in_combined_hierarchy_table(self):
        helper.add_admin_user(login="unittest")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.6.0/23")
        helper.add_instance(
            classname="ipam_subnet",
            keyname="203.0.6.0/24",
            fields={"description": "Child A description"},
        )
        helper.add_instance(classname="ipam_subnet", keyname="203.0.7.0/24")

        parent = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.6.0/23")
        response = self.client.get(
            reverse("app_data:instance_detail", kwargs={"id": parent.id}), follow=True
        )

        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Parents")
        self.assertContains(response, "Current")
        self.assertContains(response, "Childs")
        self.assertContains(response, "Child A description")
        self.assertContains(response, "203.0.7.0/24")
        # self row: bold/starred, shows up once as the current instance
        self.assertContains(response, "203.0.6.0/23")

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_no_children_shows_none_in_child_section(self):
        helper.add_admin_user(login="unittest")
        helper.add_instance(classname="ipam_subnet", keyname="203.0.8.0/24")

        leaf = Instance.from_keyname(classname="ipam_subnet", keyname="203.0.8.0/24")
        response = self.client.get(
            reverse("app_data:instance_detail", kwargs={"id": leaf.id}), follow=True
        )

        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Childs")
        # only the child section is empty here - 203.0.8.0/24 has the
        # builtin 0.0.0.0/0 as an ancestor, so "None" appears exactly once
        self.assertContains(response, "None", count=1)

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_no_ancestors_shows_none_in_parent_section(self):
        helper.add_admin_user(login="unittest")

        root = Instance.from_keyname(classname="ipam_subnet", keyname="0.0.0.0/0")
        self.assertIsNotNone(root)

        response = self.client.get(
            reverse("app_data:instance_detail", kwargs={"id": root.id}), follow=True
        )

        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Parents")
        self.assertContains(response, "None")


class IpamSubnetCurrentRowStandoutTest(TestCase):
    def _render_row(self, is_containing):
        row = {
            "id": 1,
            "keyname": "1.2.3.0/24",
            "description": "",
            "prefixlen": 24,
            "occupancy": {"count": 0, "percent": 0.0},
            "vlans": [],
            "is_containing": is_containing,
        }
        return render_to_string("app_data/ipam/_subnet_hierarchy_row.html", {"row": row})

    def test_current_row_is_bold_with_asterisk(self):
        html = self._render_row(is_containing=True)
        self.assertIn("fw-bold", html)
        self.assertIn("fa-asterisk", html)

    def test_other_rows_have_no_standout(self):
        html = self._render_row(is_containing=False)
        self.assertNotIn("fw-bold", html)
        self.assertNotIn("fa-asterisk", html)
