"""Track A — Identity & basics field-by-field tests.

Walks the 12 "header block" fields of ``affiliate.widget.config`` and
verifies each one observably affects the public widget endpoints.

The point of this file is to catch a configuration field that's stored
in the DB but doesn't actually drive widget behaviour — the kind of bug
where an affiliate flips a backend toggle, sees no change in their
widget, and files a support ticket.

Found-and-fixed during the smoke pass that led to this file:
- ``primary_color`` / ``text_color`` were stored but the widget JS never
  consumed them (the generated palette did the rendering, the explicit
  fields were silently ignored). Fix landed in ``kj_widget.js`` —
  the regression check here is API-side only; the JS render contract
  is covered by manual Playwright sweeps until a JS unit test exists.

Tracks B–E are stubbed at the bottom of this file with
``@unittest.skip`` so a future run picks them up and we don't lose
track of the matrix.
"""

import json
import unittest

from odoo.tests import tagged

from odoo.addons.kj_affiliate_widget.controllers import widget_api as _wa
from odoo.addons.kj_affiliate_widget.tests.common import WidgetTestCommon


@tagged("post_install", "-at_install", "kj_affiliate_widget", "A")
class TestConfigTrackABasics(WidgetTestCommon):
    """One test per Track A field — flip the field, hit the endpoint,
    assert the observable matches. Reset is handled by ``setUp``
    (cache clear) and the test transaction rollback (DB clear).
    """

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _get_config_json(self, key=None, mode=None):
        """GET /api/widget/config/<key> and return the parsed JSON."""
        key = key or self.config.widget_key
        url = f"/api/widget/config/{key}"
        if mode:
            url += f"?mode={mode}"
        r = self.url_open(url)
        self.assertEqual(
            r.status_code, 200,
            f"Expected 200 from config endpoint, got {r.status_code}: {r.text[:200]}",
        )
        return r.json()

    def _post_event(self, event_type, key=None, **extra):
        """POST /api/widget/event and return the response."""
        payload = {"key": key or self.config.widget_key, "type": event_type}
        payload.update(extra)
        return self.url_open(
            "/api/widget/event",
            data=json.dumps(payload),
            headers={"Content-Type": "application/json"},
        )

    # ------------------------------------------------------------------
    # 1. user_id — the affiliate owner. The endpoint doesn't surface it
    # in the JSON, but a malformed user_id should still allow the row to
    # exist + serve. Sanity: the config we created has the user we set.
    # ------------------------------------------------------------------

    def test_01_user_id_set_on_config(self):
        self.assertEqual(self.config.user_id, self.affiliate_user)
        # The endpoint serves the config regardless of who owns it
        # (public widget — owner only matters for ACL on the backend form).
        data = self._get_config_json()
        self.assertEqual(data["key"], self.config.widget_key)

    # ------------------------------------------------------------------
    # 2. partner_id is the affiliate link on the model, but it is PII and
    # must never leak through the public /api/widget/config endpoint.
    # ------------------------------------------------------------------

    def test_02_partner_id_not_leaked_in_public_api(self):
        data = self._get_config_json()
        self.assertNotIn(
            "partner_id", data,
            "partner_id is the affiliate link — it must not appear in the "
            "public /api/widget/config response (PII leak). If you added "
            "it, audit the read sites.",
        )

    # ------------------------------------------------------------------
    # 3. widget_key — bad key → 403; good → 200.
    # ------------------------------------------------------------------

    def test_03_widget_key_invalid_returns_403(self):
        r = self.url_open("/api/widget/config/this-key-does-not-exist")
        self.assertEqual(r.status_code, 403)

    def test_03b_widget_key_valid_returns_200(self):
        r = self.url_open(f"/api/widget/config/{self.config.widget_key}")
        self.assertEqual(r.status_code, 200)

    # ------------------------------------------------------------------
    # 4. affiliate_website — admin label only. Writable, persists,
    # doesn't break the endpoint.
    # ------------------------------------------------------------------

    def test_04_affiliate_website_writable_and_persists(self):
        self.config.affiliate_website = "https://test.example.com"
        self.assertEqual(self.config.affiliate_website, "https://test.example.com")
        # Endpoint still serves.
        data = self._get_config_json()
        self.assertEqual(data["key"], self.config.widget_key)

    # ------------------------------------------------------------------
    # 5. is_active — flip to False → endpoint refuses to serve.
    # ------------------------------------------------------------------

    def test_05_is_active_false_returns_403(self):
        self.config.is_active = False
        r = self.url_open(f"/api/widget/config/{self.config.widget_key}")
        self.assertEqual(r.status_code, 403)
        # And the fragment endpoint too.
        r = self.url_open(
            f"/api/widget/fragment?key={self.config.widget_key}&path=/de/job-offers"
        )
        self.assertEqual(r.status_code, 403)

    # ------------------------------------------------------------------
    # 6. widget_mode — set + observe via the API field.
    # ------------------------------------------------------------------

    def test_06_widget_mode_returned_in_config(self):
        self.config.widget_mode = "overlay"
        data = self._get_config_json()
        self.assertEqual(data["mode"], "overlay")
        self.config.widget_mode = "inline"
        data = self._get_config_json()
        self.assertEqual(data["mode"], "inline")

    # ------------------------------------------------------------------
    # 7. language — set + observe.
    # ------------------------------------------------------------------

    def test_07_language_returned_in_config(self):
        self.config.language = "en"
        data = self._get_config_json()
        self.assertEqual(data["lang"], "en")
        self.config.language = "de"
        data = self._get_config_json()
        self.assertEqual(data["lang"], "de")

    # ------------------------------------------------------------------
    # 8. landing_page — set + observe.
    # ------------------------------------------------------------------

    def test_08_landing_page_returned_in_config(self):
        # Selection values defined on the field: /, /job-offers, /company-profiles
        self.config.landing_page = "/company-profiles"
        data = self._get_config_json()
        self.assertEqual(data["page"], "/company-profiles")
        self.config.landing_page = "/job-offers"
        data = self._get_config_json()
        self.assertEqual(data["page"], "/job-offers")

    # ------------------------------------------------------------------
    # 9. primary_color — controller side. JS-side render contract is
    # not testable in HttpCase; verified manually post-fix.
    # ------------------------------------------------------------------

    def test_09_primary_color_returned_in_config(self):
        self.config.primary_color = "#00ff00"
        data = self._get_config_json()
        self.assertEqual(
            data["color"], "#00ff00",
            "Controller must propagate primary_color verbatim as `color`. "
            "If this passes but the widget renders the wrong colour in a "
            "browser, the JS-side wire-up has regressed — see the "
            "_afterConfig palette block in kj_widget.js.",
        )

    # ------------------------------------------------------------------
    # 10. text_color — controller side, same shape as primary_color.
    # ------------------------------------------------------------------

    def test_10_text_color_returned_in_config(self):
        self.config.text_color = "#123456"
        data = self._get_config_json()
        self.assertEqual(data["text_color"], "#123456")

    # ------------------------------------------------------------------
    # 11. total_impressions — fire an event → flush → assert counter.
    # ------------------------------------------------------------------

    def test_11_total_impressions_increments_after_event(self):
        before = self.config.total_impressions
        r = self._post_event("impression", referrer="http://aff.example/page")
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.json().get("ok"), True)
        _wa._flush_event_queue()
        self.config.invalidate_recordset(["total_impressions"])
        self.assertEqual(
            self.config.total_impressions, before + 1,
            f"Expected total_impressions to go from {before} to {before + 1} "
            f"after one event, got {self.config.total_impressions}",
        )

    # ------------------------------------------------------------------
    # 12. total_clicks — fire a click event → flush → assert counter.
    # ------------------------------------------------------------------

    def test_12_total_clicks_increments_after_event(self):
        before = self.config.total_clicks
        r = self._post_event(
            "click",
            job_id=self.job.id,
            referrer="http://aff.example/page",
        )
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.json().get("ok"), True)
        _wa._flush_event_queue()
        self.config.invalidate_recordset(["total_clicks"])
        self.assertEqual(
            self.config.total_clicks, before + 1,
            f"Expected total_clicks to go from {before} to {before + 1} "
            f"after one click, got {self.config.total_clicks}",
        )


# ----------------------------------------------------------------------
# Deferred tracks — stubbed so a future run rediscovers them.
# See /Users/mac/.claude/plans/please-check-kj-affiliate-widget-what-luminous-giraffe.md
# ----------------------------------------------------------------------


# Track B (filters) lives in test_config_track_b_filters.py — keeping
# each track in its own file so failures group cleanly and adding more
# tests to one track doesn't force re-reading the others.


# Track C (theme) lives in test_config_track_c_theme.py.


# Track D (mode-specific) lives in test_config_track_d_mode_specific.py.


@tagged("post_install", "-at_install", "kj_affiliate_widget", "E")
class TestConfigTrackEComputed(WidgetTestCommon):

    @unittest.skip("deferred — see plan file (Track E: derived/computed)")
    def test_track_e_placeholder(self):
        pass
