# (c) cavaliba.com - test / search / searchbar

from django.test import TestCase, override_settings
from django.urls import reverse

import app_home.cache as cache
from app_data.search import searchbar
from tests import helper

ADMIN_AAA = {"perms": ["p_data_admin"]}
NO_PERM_AAA = {"perms": []}

SB_SCHEMA = "sb_test"

SB_FIELDS = {
    "description": {"displayname": "Description", "dataformat": "string"},
    "notes": {"displayname": "Notes", "dataformat": "string"},
}


class SearchbarQueryTest(TestCase):
    def setUp(self):
        """Fresh schema/instances per test - description/notes fields cover
        the description-EAV and full-json search stages respectively."""
        cache.clear()
        helper.add_schema(
            classname=SB_SCHEMA, displayname="SB Test Schema", field_definition=SB_FIELDS
        )

        helper.add_instance(
            classname=SB_SCHEMA, keyname="obj-mkeyname",
            fields={"displayname": "Instance KN", "description": "none", "notes": "none"},
        )  # fmt: skip
        helper.add_instance(
            classname=SB_SCHEMA, keyname="obj-b2",
            fields={"displayname": "Instance MDISPLAY name", "description": "none", "notes": "none"},
        )  # fmt: skip
        helper.add_instance(
            classname=SB_SCHEMA, keyname="obj-c3",
            fields={"displayname": "Instance DE", "description": "marker mdescript value", "notes": "none"},
        )  # fmt: skip
        helper.add_instance(
            classname=SB_SCHEMA, keyname="obj-d4",
            fields={"displayname": "Instance JS", "description": "none", "notes": "marker mjsonval here"},
        )  # fmt: skip
        helper.add_instance(
            classname=SB_SCHEMA, keyname="dupterm-key",
            fields={"displayname": "dupterm-key-and-name", "description": "none", "notes": "none"},
        )  # fmt: skip

    def test_keyname_match(self):
        reply = searchbar(query="mkeyname", aaa=ADMIN_AAA)
        self.assertIsNone(reply["error"])
        keynames = {r["keyname"]: r["match"] for r in reply["results"]}
        self.assertEqual(keynames.get("obj-mkeyname"), "keyname")

    def test_displayname_match(self):
        reply = searchbar(query="mdisplay", aaa=ADMIN_AAA)
        keynames = {r["keyname"]: r["match"] for r in reply["results"]}
        self.assertEqual(keynames.get("obj-b2"), "displayname")

    def test_description_match(self):
        reply = searchbar(query="mdescript", aaa=ADMIN_AAA)
        keynames = {r["keyname"]: r["match"] for r in reply["results"]}
        self.assertEqual(keynames.get("obj-c3"), "description")
        result = next(r for r in reply["results"] if r["keyname"] == "obj-c3")
        self.assertIn("mdescript", result["description"])

    def test_json_match(self):
        reply = searchbar(query="mjsonval", aaa=ADMIN_AAA)
        keynames = {r["keyname"]: r["match"] for r in reply["results"]}
        self.assertEqual(keynames.get("obj-d4"), "json")

    def test_displayname_falls_back_to_keyname_when_blank(self):
        helper.add_instance(classname=SB_SCHEMA, keyname="mnoname-instance")
        reply = searchbar(query="mnoname", aaa=ADMIN_AAA)
        result = next(r for r in reply["results"] if r["keyname"] == "mnoname-instance")
        self.assertEqual(result["displayname"], "mnoname-instance")

    def test_result_carries_schema_icon(self):
        helper.add_instance(
            classname="site", keyname="site-micontest", fields={"displayname": "Site Icon"}
        )
        reply = searchbar(query="micontest", aaa=ADMIN_AAA)
        result = next(r for r in reply["results"] if r["keyname"] == "site-micontest")
        self.assertEqual(result["icon"], "fa-hospital-o")

    def test_result_icon_falls_back_when_schema_has_none(self):
        reply = searchbar(query="mkeyname", aaa=ADMIN_AAA)
        result = next(r for r in reply["results"] if r["keyname"] == "obj-mkeyname")
        self.assertEqual(result["icon"], "fa-question")

    def test_dedup_shows_once_in_earliest_match(self):
        reply = searchbar(query="dupterm", aaa=ADMIN_AAA)
        matches = [r for r in reply["results"] if r["keyname"] == "dupterm-key"]
        self.assertEqual(len(matches), 1)
        self.assertEqual(matches[0]["match"], "keyname")

    def test_schema_prefix_scopes_to_one_schema(self):
        helper.add_schema(classname="sb_other", displayname="SB Other")
        helper.add_instance(
            classname="sb_other", keyname="other-mkeyname", fields={"displayname": "Other KN"}
        )
        reply = searchbar(query=f"{SB_SCHEMA}:mkeyname", aaa=ADMIN_AAA)
        classnames = {r["classname"] for r in reply["results"]}
        self.assertEqual(classnames, {SB_SCHEMA})

    def test_schema_prefix_unknown_schema_falls_back_to_literal(self):
        reply = searchbar(query="doesnotexist:mkeyname", aaa=ADMIN_AAA)
        self.assertIsNone(reply["error"])
        # literal "doesnotexist:mkeyname" isn't found anywhere - no crash, no results
        self.assertEqual(reply["results"], [])

    def test_schema_name_match(self):
        helper.add_schema(
            classname="sb_zzz_target", displayname="ZzzUniqueSchemaName",
            options={"description": "Schema level description text"},
        )  # fmt: skip
        reply = searchbar(query="zzzuniqueschema", aaa=ADMIN_AAA)
        schema_hits = [r for r in reply["results"] if r["type"] == "schema"]
        self.assertEqual(len(schema_hits), 1)
        self.assertEqual(schema_hits[0]["classname"], "sb_zzz_target")
        self.assertEqual(schema_hits[0]["description"], "Schema level description text")

    def test_min_length_error(self):
        reply = searchbar(query="ab", aaa=ADMIN_AAA)
        self.assertIsNotNone(reply["error"])
        self.assertEqual(reply["results"], [])

    def test_dangerous_characters_rejected(self):
        for bad_query in ["<script>alert(1)</script>", "'; DROP TABLE data_instance; --", "50%"]:
            reply = searchbar(query=bad_query, aaa=ADMIN_AAA)
            self.assertIsNotNone(reply["error"], msg=bad_query)
            self.assertEqual(reply["results"], [], msg=bad_query)

    def test_empty_and_none_query(self):
        for q in ["", None, "   "]:
            reply = searchbar(query=q, aaa=ADMIN_AAA)
            self.assertEqual(reply["results"], [])

    @override_settings(SEARCHBAR_MAX_ANSWER=3)
    def test_truncation(self):
        for i in range(5):
            helper.add_instance(
                classname=SB_SCHEMA, keyname=f"cap-target-{i}", fields={"displayname": f"cap {i}"}
            )
        reply = searchbar(query="cap-target", aaa=ADMIN_AAA)
        self.assertEqual(len(reply["results"]), 3)
        self.assertTrue(reply["truncated"])

    def test_read_permission_filters_results(self):
        helper.add_instance(
            classname="site", keyname="site-mkeyname", fields={"displayname": "Site KN"}
        )

        reply_denied = searchbar(query="mkeyname", aaa=NO_PERM_AAA)
        self.assertEqual(reply_denied["results"], [])

        reply_allowed = searchbar(query="mkeyname", aaa={"perms": ["p_data_site_read"]})
        keynames = {r["keyname"] for r in reply_allowed["results"]}
        self.assertIn("site-mkeyname", keynames)


class SearchbarIpamTest(TestCase):
    def setUp(self):
        """ipam_ip is auto-derived from an ipv4-format field; ipam_subnet can
        be created directly with a CIDR literal as keyname."""
        cache.clear()
        helper.add_schema(
            classname="sb_ipam_host",
            field_definition={"ip_address": {"dataformat": "ipv4", "displayname": "IP"}},
        )
        helper.add_instance(
            classname="sb_ipam_host", keyname="host01", fields={"ip_address": "203.0.113.77"}
        )
        helper.add_instance(classname="ipam_subnet", keyname="203.0.114.0/24")

    def test_ipam_ip_partial_match(self):
        reply = searchbar(query="203.0.113", aaa=ADMIN_AAA)
        classnames = {(r["classname"], r["keyname"]) for r in reply["results"]}
        self.assertTrue(any(c == "ipam_ip" for c, _k in classnames))

    def test_ipam_ip_full_match(self):
        reply = searchbar(query="203.0.113.77", aaa=ADMIN_AAA)
        displaynames = {r["displayname"] for r in reply["results"] if r["classname"] == "ipam_ip"}
        self.assertIn("203.0.113.77", displaynames)

    def test_ipam_subnet_cidr_match(self):
        reply = searchbar(query="203.0.114.0/24", aaa=ADMIN_AAA)
        keynames = {r["keyname"] for r in reply["results"] if r["classname"] == "ipam_subnet"}
        self.assertIn("203.0.114.0/24", keynames)

    def test_ipam_subnet_partial_cidr_match(self):
        reply = searchbar(query="203.0.114", aaa=ADMIN_AAA)
        keynames = {r["keyname"] for r in reply["results"] if r["classname"] == "ipam_subnet"}
        self.assertIn("203.0.114.0/24", keynames)


class SearchbarViewTest(TestCase):
    def setUp(self):
        """cache.clear() so schema/permission lookups reflect fixtures per test."""
        cache.clear()

    def test_view_requires_authentication(self):
        """p_data_access is granted to every authenticated user via the builtin
        role_default (see builtin/init/012_role.yml) - it gates authenticated
        vs anonymous access, so an anonymous request is what actually exercises
        the redirect, same as every other app_data view gated on p_data_access."""
        response = self.client.get(reverse("app_data:searchbar"), {"query": "abc"})
        self.assertEqual(response.status_code, 302)

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_view_renders_results_for_authorized_user(self):
        helper.add_admin_user(login="unittest")
        helper.add_instance(
            classname="site", keyname="site-viewtest", fields={"displayname": "Site View KN"}
        )

        response = self.client.get(reverse("app_data:searchbar"), {"query": "viewtest"})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Site View KN")
        self.assertContains(response, "site-viewtest")

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_view_empty_query_shows_prompt_not_results(self):
        helper.add_admin_user(login="unittest")
        response = self.client.get(reverse("app_data:searchbar"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "navbar")

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_view_dangerous_query_shows_error_escaped(self):
        helper.add_admin_user(login="unittest")
        response = self.client.get(
            reverse("app_data:searchbar"), {"query": "<script>alert(1)</script>"}
        )
        self.assertEqual(response.status_code, 200)
        self.assertNotContains(response, "<script>alert(1)</script>")

    @override_settings(CAVALIBA_AUTH_MODE="unittest")
    def test_ui_perm_searchbar_context_flag(self):
        helper.add_admin_user(login="unittest")
        response = self.client.get(reverse("app_home:private"))
        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.context["ui_perm_searchbar"])
