# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import hashlib
import hmac
import re
import time
from urllib.parse import unquote

from odoo.fields import Date

from odoo.tests import HttpCase, tagged


@tagged("post_install", "-at_install")
class TestMembershipSignupTour(HttpCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.website = cls.env.ref("website.default_website")
        cls.website.auth_signup_uninvited = "b2b"
        cls.website.membership_signup_required = True
        today = Date.to_date(Date.today())
        cls.membership_product = cls.env["product.template"].create({
            "name": "Membership Test Product",
            "membership": True,
            "list_price": 60.0,
            "is_published": True,
            "sale_ok": True,
            "website_id": cls.website.id,
            "membership_date_from": today.replace(month=1, day=1),
            "membership_date_to": today.replace(month=12, day=31),
        })
        cls.regular_product = cls.env["product.template"].create({
            "name": "Regular Test Product",
            "membership": False,
            "list_price": 5.0,
            "is_published": True,
            "sale_ok": True,
            "website_id": cls.website.id,
        })
        # Enable the custom/transfer payment provider if it is installed so the
        # cart page renders fully (no checkout/payment is exercised by the
        # tours, but it avoids warnings during cart rendering).
        provider = cls.env.ref("payment.payment_provider_transfer", raise_if_not_found=False)
        if provider and provider.state != "enabled":
            provider.write({"state": "enabled", "is_published": True})

    def test_01_membership_signup_flow(self):
        self.start_tour(
            self.env["website"].get_client_action_url("/shop"),
            "website_membership_signup_required_tour",
            login=None,
        )
        # The tour submitted the signup form, so a new portal user must exist
        # and the cart of that user must contain the membership product.
        users = self.env["res.users"].search([
            ("login", "like", "membership.tour.%@test.example.com"),
        ])
        self.assertTrue(users, "Tour should have created a user via /web/signup")
        first = users.sorted("create_date")[-1]
        self.assertTrue(first.partner_id.firstname)
        self.assertTrue(first.partner_id.lastname)
        carts = self.env["sale.order"].search([
            ("partner_id", "=", first.partner_id.id),
            ("state", "=", "draft"),
        ])
        self.assertTrue(carts)
        order_lines = carts.mapped("order_line").filtered(
            lambda l: l.product_id.product_tmpl_id == self.membership_product
        )
        self.assertTrue(order_lines, "Cart must contain the membership product")

    def test_02_non_membership_no_intercept(self):
        self.start_tour(
            self.env["website"].get_client_action_url("/shop"),
            "website_membership_signup_required_non_intercept_tour",
            login=None,
        )

    # -- HTTP-level tests (no Chrome) --------------------------------------

    _CSRF_RE = re.compile(r'name="csrf_token"\s+value="([^"]+)"')

    def _compute_csrf_token(self):
        """Manually compute a valid csrf token bound to the opener's current
        session_id cookie. Mirrors `request.csrf_token()` so the test client
        does not need to scrape the rendered HTML (which some addons strip for
        specific authenticated users).
        """
        sid = self.opener.cookies.get("session_id")
        if not sid:
            return None
        secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
        max_ts = int(time.time() + 31536000)  # 1y, matches Odoo's default.
        msg = f"{sid}{max_ts}".encode("utf-8")
        hm = hmac.new(secret.encode("ascii"), msg, hashlib.sha1).hexdigest()
        return f"{hm}o{max_ts}"

    def _cart_update(self, product, add_qty="1"):
        """POST to /shop/cart/update with a valid csrf token. The csrf token
        is scraped from the rendered product page, with a fallback computed
        directly from the opener's session_id cookie (covers sessions whose
        rendered page strips the form for the logged user).
        """
        product_url = "/shop/%s" % self.env["ir.http"]._slug(product)
        page = self.url_open(product_url, allow_redirects=True)
        self.assertEqual(
            page.status_code, 200,
            "Product page must render (got %s for %s)" % (page.status_code, product_url),
        )
        match = self._CSRF_RE.search(page.text)
        csrf_token = match.group(1) if match else self._compute_csrf_token()
        self.assertTrue(
            csrf_token,
            "Could not obtain a csrf_token (rendered page or session-based).",
        )
        return self.url_open(
            "/shop/cart/update",
            data={
                "product_id": str(product.product_variant_id.id),
                "add_qty": add_qty,
                "csrf_token": csrf_token,
            },
            allow_redirects=False,
        )

    def _assert_redirect(self, resp):
        self.assertTrue(
            300 <= resp.status_code < 400,
            "Expected a HTTP redirect, got %s. Location: %s"
            % (resp.status_code, resp.headers.get("Location", "")),
        )

    def test_03_anon_membership_redirect(self):
        """RF-01 / RF-02: anonymous POST /shop/cart/update for a membership
        product must redirect to /web/signup?membership_signup=1&redirect=...
        without adding the product to the cart.
        """
        resp = self._cart_update(self.membership_product)
        self._assert_redirect(resp)
        # The `Location` header is URL-encoded, so any path segment with
        # non-safe chars (here the product slug) arrives percent-encoded.
        # Assert on stable substrings that do not depend on the encoding of
        # each part of the URL (RF-02).
        location = resp.headers.get("Location", "")
        self.assertIn("/web/signup", location)
        self.assertIn("membership_signup=1", location)
        self.assertIn("redirect=", location)
        self.assertIn("membership-test-product", unquote(location))

    def test_04_anon_regular_no_redirect(self):
        """RF-05: anonymous POST /shop/cart/update for a non-membership
        product must follow the core flow (redirect to /shop/cart).
        """
        resp = self._cart_update(self.regular_product)
        self._assert_redirect(resp)
        location = resp.headers.get("Location", "")
        self.assertIn("/shop/cart", location)
        self.assertNotIn("/web/signup", location)

    def test_05_authenticated_membership_no_redirect(self):
        """RF-06: an authenticated (non-public) user adding a membership
        product must NOT be redirected to /web/signup.
        """
        self.authenticate("admin", "admin")
        resp = self._cart_update(self.membership_product)
        self._assert_redirect(resp)
        location = resp.headers.get("Location", "")
        self.assertIn("/shop/cart", location)
        self.assertNotIn("/web/signup", location)

    def test_06_signup_marker_page_renders(self):
        """RF-03: GET /web/signup?membership_signup=1&redirect=/shop/... must
        return 200 (not 404) on a b2b website thanks to the conditional
        override of `_get_signup_invitation_scope`.
        """
        product_url = "/shop/%s" % self.env["ir.http"]._slug(self.membership_product)
        params = {"membership_signup": "1", "redirect": product_url}
        resp = self.url_open(
            "/web/signup?" + "&".join(f"{k}={v}" for k, v in params.items()),
            allow_redirects=False,
        )
        self.assertIn(resp.status_code, (200, 302))
        if resp.status_code == 302:
            # Could be redirected to login if a session already exists.
            self.assertNotIn("web/login", resp.headers.get("Location", ""))

    def test_07_disabled_intercept_anon_membership(self):
        """RF-07: with `membership_signup_required = False` on the website, an
        anonymous POST /shop/cart/update for a membership product must follow
        the core flow (no redirect to /web/signup).
        """
        self.website.membership_signup_required = False
        try:
            resp = self._cart_update(self.membership_product)
            self._assert_redirect(resp)
            location = resp.headers.get("Location", "")
            self.assertIn("/shop/cart", location)
            self.assertNotIn("/web/signup", location)
        finally:
            self.website.membership_signup_required = True