"""Track B — Filter fields (filter_mode + 6 filter_*).

The filter fields are the highest production risk in the config matrix:
an affiliate's curated list is supposed to limit what visitors see, and
a leak here means visitors see jobs the affiliate never paid for. The
existing ``test_widget_apply.py::test_curated_filter_enforced`` pins
the apply-side (POST) enforcement. This file pins the symmetric
read-side at ``/api/widget/jobs`` — the list visitors actually browse.

For each test:
1. Create two hr.job rows with distinguishable, test-only properties.
2. Set the relevant filter on ``self.config``.
3. GET /api/widget/jobs?key=... and assert the returned ids match.

If a custom-field domain target (e.g. ``hr.job.fields``) isn't present
in this checkout, the test skips with a reason instead of erroring.
"""

import json

from odoo.tests import tagged

from odoo.addons.kj_affiliate_widget.tests.common import WidgetTestCommon


@tagged("post_install", "-at_install", "kj_affiliate_widget", "B")
class TestConfigTrackBFilters(WidgetTestCommon):

    # ------------------------------------------------------------------
    # Setup helpers — each test creates the jobs it needs so the
    # transaction rollback cleans up automatically.
    # ------------------------------------------------------------------

    def _mk_job(self, **vals):
        """Create a fresh published hr.job for this test."""
        defaults = {
            "name": "TrackB Job",
            "company_id": self.company.id,
            "is_published": True,
            "active": True,
        }
        defaults.update(vals)
        return self.env["hr.job"].create(defaults)

    def _jobs(self, key=None):
        """GET /api/widget/jobs and return the (status, ids-set, total)."""
        key = key or self.config.widget_key
        r = self.url_open(f"/api/widget/jobs?key={key}&page_size=24")
        self.assertEqual(
            r.status_code, 200,
            f"/api/widget/jobs failed: {r.status_code} {r.text[:200]}",
        )
        data = json.loads(r.text)
        return data["total"], {j["id"] for j in data["jobs"]}

    # ------------------------------------------------------------------
    # filter_mode = all
    # ------------------------------------------------------------------

    def test_b01_filter_mode_all_returns_jobs(self):
        """Default 'all' returns the suite's job among the published set."""
        self.config.filter_mode = "all"
        total, ids = self._jobs()
        self.assertIn(
            self.job.id, ids,
            "filter_mode=all should expose the suite's published job",
        )

    # ------------------------------------------------------------------
    # filter_mode = curated
    # ------------------------------------------------------------------

    def test_b02_filter_mode_curated_restricts_to_listed_jobs(self):
        """Only jobs in filter_job_ids are returned when mode=curated."""
        other = self._mk_job(name="TrackB OtherJob")
        self.config.write({
            "filter_mode": "curated",
            "filter_job_ids": [(6, 0, [self.job.id])],
        })
        total, ids = self._jobs()
        self.assertIn(self.job.id, ids)
        self.assertNotIn(
            other.id, ids,
            "Curated mode must hide jobs that aren't in filter_job_ids — "
            "an affiliate could otherwise leak unpaid jobs into their feed.",
        )

    def test_b03_filter_mode_curated_empty_list_falls_through_to_all(self):
        """curated + empty filter_job_ids → behaves like 'all'.

        This is the documented behaviour in get_affiliate_job_domain:
        it only appends the in-clause if filter_job_ids is truthy.
        Pinning it here so a regression in that branch is loud.
        """
        other = self._mk_job(name="TrackB FallthroughJob")
        self.config.write({
            "filter_mode": "curated",
            "filter_job_ids": [(5, 0, 0)],  # clear m2m
        })
        total, ids = self._jobs()
        # Both jobs should be visible since the curated list is empty.
        self.assertIn(self.job.id, ids)
        self.assertIn(other.id, ids)

    # ------------------------------------------------------------------
    # filter_mode = filtered + filter_city (single-field test — the
    # other filter_* fields share the same code path, covered below)
    # ------------------------------------------------------------------

    def test_b04_filter_mode_filtered_by_city(self):
        """filtered + filter_city='X' returns only jobs with x_city ilike X."""
        match = self._mk_job(name="TrackB Match", x_city="TrackBVille")
        miss = self._mk_job(name="TrackB Miss", x_city="Somewhere Else")
        self.config.write({
            "filter_mode": "filtered",
            "filter_city": "TrackBVille",
        })
        total, ids = self._jobs()
        self.assertIn(match.id, ids)
        self.assertNotIn(miss.id, ids)
        # self.job has no x_city set, so it shouldn't pass the filter either.
        self.assertNotIn(
            self.job.id, ids,
            "Jobs without x_city should fail the ilike on a non-empty filter_city",
        )

    # ------------------------------------------------------------------
    # filter_mode = filtered + filter_country_ids
    # ------------------------------------------------------------------

    def test_b05_filter_mode_filtered_by_country(self):
        """filtered + filter_country_ids returns only jobs with x_country_id in list."""
        de = self.env.ref("base.de")
        fr = self.env.ref("base.fr")
        match = self._mk_job(name="TrackB DEJob", x_country_id=de.id)
        miss = self._mk_job(name="TrackB FRJob", x_country_id=fr.id)
        self.config.write({
            "filter_mode": "filtered",
            "filter_country_ids": [(6, 0, [de.id])],
        })
        total, ids = self._jobs()
        self.assertIn(match.id, ids)
        self.assertNotIn(miss.id, ids)

    # ------------------------------------------------------------------
    # filter_mode = filtered + filter_employer_ids
    # ------------------------------------------------------------------

    def test_b06_filter_mode_filtered_by_employer(self):
        """filtered + filter_employer_ids returns only jobs whose x_employer_parent_id matches."""
        Partner = self.env["res.partner"]
        emp_match = Partner.create({"name": "TrackB Match GmbH", "is_company": True})
        emp_miss = Partner.create({"name": "TrackB Miss GmbH", "is_company": True})
        match = self._mk_job(name="TrackB EmpMatch", x_employer_parent_id=emp_match.id)
        miss = self._mk_job(name="TrackB EmpMiss", x_employer_parent_id=emp_miss.id)
        self.config.write({
            "filter_mode": "filtered",
            "filter_employer_ids": [(6, 0, [emp_match.id])],
        })
        total, ids = self._jobs()
        self.assertIn(match.id, ids)
        self.assertNotIn(miss.id, ids)

    # ------------------------------------------------------------------
    # filter_mode = filtered + filter_function_ids
    # ------------------------------------------------------------------

    def test_b07_filter_mode_filtered_by_function(self):
        """filtered + filter_function_ids returns only jobs with matching x_job_function_id."""
        Func = self.env.get("hr.job.function")
        if Func is None:
            self.skipTest("hr.job.function model not installed in this checkout")
        f_match = Func.sudo().create({"x_name": "TrackB MatchFunc"})
        f_miss = Func.sudo().create({"x_name": "TrackB MissFunc"})
        match = self._mk_job(name="TrackB FuncMatch", x_job_function_id=f_match.id)
        miss = self._mk_job(name="TrackB FuncMiss", x_job_function_id=f_miss.id)
        self.config.write({
            "filter_mode": "filtered",
            "filter_function_ids": [(6, 0, [f_match.id])],
        })
        total, ids = self._jobs()
        self.assertIn(match.id, ids)
        self.assertNotIn(miss.id, ids)

    # ------------------------------------------------------------------
    # filter_mode = filtered + filter_field_ids
    # ------------------------------------------------------------------

    def test_b08_filter_mode_filtered_by_field(self):
        """filtered + filter_field_ids returns only jobs whose x_job_field_ids overlaps."""
        Field = self.env.get("hr.job.fields")
        if Field is None:
            self.skipTest("hr.job.fields model not installed in this checkout")
        f_match = Field.sudo().create({"x_name": "TrackB MatchField"})
        f_miss = Field.sudo().create({"x_name": "TrackB MissField"})
        match = self._mk_job(
            name="TrackB FieldMatch",
            x_job_field_ids=[(6, 0, [f_match.id])],
        )
        miss = self._mk_job(
            name="TrackB FieldMiss",
            x_job_field_ids=[(6, 0, [f_miss.id])],
        )
        self.config.write({
            "filter_mode": "filtered",
            "filter_field_ids": [(6, 0, [f_match.id])],
        })
        total, ids = self._jobs()
        self.assertIn(match.id, ids)
        self.assertNotIn(miss.id, ids)

    # ------------------------------------------------------------------
    # Defence-in-depth: read endpoint mirrors apply endpoint
    # ------------------------------------------------------------------

    def test_b09_read_and_apply_endpoints_agree_on_curated(self):
        """A curated config must hide a non-listed job from BOTH the read
        endpoint (/api/widget/jobs) and the apply endpoint. If they ever
        disagree, the widget UI shows a job the visitor can't actually
        apply to (or vice versa) — both are visitor-facing bugs.
        """
        other = self._mk_job(name="TrackB CrossCheck")
        self.config.write({
            "filter_mode": "curated",
            "filter_job_ids": [(6, 0, [self.job.id])],
        })
        total, ids = self._jobs()
        self.assertNotIn(other.id, ids, "Read endpoint leaked a non-curated job")
        # Apply side already tested in test_widget_apply.test_curated_filter_enforced.
