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

import werkzeug.datastructures

from odoo.exceptions import UserError
from odoo.fields import Date

from odoo.addons.website_membership_signup_required.controllers.main import (
    AuthSignupHomeMembership as AuthSignupHome,
)
from odoo.addons.website_membership_signup_required.controllers.website_sale import (
    WebsiteSaleMembershipSignup,
)
from odoo.addons.website.tools import MockRequest
from odoo.tests import TransactionCase, tagged


@tagged("post_install", "-at_install")
class TestMembershipSignup(TransactionCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.website = cls.env.ref("website.default_website")
        cls.website.auth_signup_uninvited = "b2b"
        today = Date.to_date(Date.today())
        cls.membership_product = cls.env["product.product"].create({
            "name": "Membership Card",
            "membership": True,
            "list_price": 50.0,
            "membership_date_from": today.replace(month=1, day=1),
            "membership_date_to": today.replace(month=12, day=31),
        })
        cls.regular_product = cls.env["product.product"].create({
            "name": "Regular Mug",
            "membership": False,
            "list_price": 10.0,
        })
        cls.no_user_partner = cls.env["res.partner"].create({
            "firstname": "Backend",
            "lastname": "Partner",
        })

    # -- RF-07 / RF-default ------------------------------------------------

    def test_01_website_default_flag(self):
        new_website = self.env["website"].create({"name": "Aux site"})
        self.assertTrue(new_website.membership_signup_required)

    # -- RF-01 helper -------------------------------------------------------

    def test_02_is_required_membership_product_anonymous(self):
        self.assertTrue(
            self.website._membership_signup_required_for(
                self.membership_product.id, is_public=True
            )
        )

    # -- RF-05 -------------------------------------------------------------

    def test_03_is_required_non_membership_product(self):
        self.assertFalse(
            self.website._membership_signup_required_for(
                self.regular_product.id, is_public=True
            )
        )

    # -- RF-06 -------------------------------------------------------------

    def test_04_is_required_authenticated_user(self):
        self.assertFalse(
            self.website._membership_signup_required_for(
                self.membership_product.id, is_public=False
            )
        )

    # -- RF-07 -------------------------------------------------------------

    def test_05_is_required_disabled_on_website(self):
        self.website.membership_signup_required = False
        self.assertFalse(
            self.website._membership_signup_required_for(
                self.membership_product.id, is_public=True
            )
        )

    # -- RF-03 -------------------------------------------------------------

    def test_06_signup_scope_override_with_marker(self):
        Users = self.env["res.users"]
        # No marker -> delegates to super (b2b because of setUpClass).
        with MockRequest(self.env, website=self.website) as mock_request:
            mock_request.httprequest.args = werkzeug.datastructures.MultiDict()
            self.assertEqual(Users._get_signup_invitation_scope(), "b2b")
        # Query param marker -> b2c.
        with MockRequest(self.env, website=self.website) as mock_request:
            mock_request.httprequest.args = werkzeug.datastructures.MultiDict(
                {"membership_signup": "1"}
            )
            self.assertEqual(Users._get_signup_invitation_scope(), "b2c")
        # Session flag marker -> b2c.
        with MockRequest(self.env, website=self.website) as mock_request:
            mock_request.httprequest.args = werkzeug.datastructures.MultiDict()
            mock_request.session["membership_signup_active"] = True
            self.assertEqual(Users._get_signup_invitation_scope(), "b2c")
        # Marker removed again -> back to b2b.
        with MockRequest(self.env, website=self.website) as mock_request:
            mock_request.httprequest.args = werkzeug.datastructures.MultiDict()
            self.assertEqual(Users._get_signup_invitation_scope(), "b2b")

    # -- RF-09 -------------------------------------------------------------

    def test_07_prepare_signup_values_firstname_lastname(self):
        controller = AuthSignupHome()
        qcontext = {
            "login": "joohn.doe@example.com",
            "password": "verystrongpwd",
            "confirm_password": "verystrongpwd",
            "firstname": "John",
            "lastname": "Doe",
            "accepted_terms": "accepted",
        }
        with MockRequest(self.env, website=self.website):
            values = controller._prepare_signup_values(qcontext)
        self.assertEqual(values.get("firstname"), "John")
        self.assertEqual(values.get("lastname"), "Doe")
        # partner_firstname: `name` is composed (lastname + firstname, or
        # firstname + lastname depending on _get_names_order) and added by
        # our override so that the core signup check (no name or partner)
        # passes. partner_firstname's `res.partner.create` will later drop
        # this composed `name` and recompute it from the split fields.
        self.assertIn("name", values)
        self.assertIn("John", values["name"])
        self.assertIn("Doe", values["name"])

    def test_07b_prepare_signup_values_reject_without_terms(self):
        """Signup must be rejected if the legal terms checkbox is not
        checked (`accepted_terms` missing or not 'accepted').
        """
        controller = AuthSignupHome()
        base_qcontext = {
            "login": "no.terms@example.com",
            "password": "verystrongpwd",
            "confirm_password": "verystrongpwd",
            "firstname": "No",
            "lastname": "Terms",
        }
        # Missing the key entirely.
        with MockRequest(self.env, website=self.website):
            with self.assertRaises(UserError):
                controller._prepare_signup_values(dict(base_qcontext))
        # Present but not 'accepted'.
        qcontext = dict(base_qcontext, accepted_terms="")
        with MockRequest(self.env, website=self.website):
            with self.assertRaises(UserError):
                controller._prepare_signup_values(qcontext)
        # Present and 'accepted' -> does not raise.
        qcontext = dict(base_qcontext, accepted_terms="accepted")
        with MockRequest(self.env, website=self.website):
            values = controller._prepare_signup_values(qcontext)
        self.assertEqual(values.get("firstname"), "No")

    def test_07c_prepare_signup_values_reset_password_allows_missing_terms(self):
        """Password reset (`/web/reset_password`) reuses core's
        `_prepare_signup_values`. The reset form intentionally does not
        include the legal terms checkbox, so our override must not reject
        the submission when `accepted_terms` is absent.
        """
        controller = AuthSignupHome()
        base_qcontext = {
            "login": "reset.me@example.com",
            "password": "newverystrongpwd",
            "confirm_password": "newverystrongpwd",
            "firstname": "Reset",
            "lastname": "User",
            "token": "dummy-reset-token",
        }
        # Missing `accepted_terms` on the reset path must NOT raise.
        with MockRequest(
            self.env, website=self.website, path="/web/reset_password"
        ) as mock_request:
            # The helper accepts `path=` but the underlying httprequest mock
            # also exposes the path attribute; double-check alignment.
            self.assertEqual(mock_request.httprequest.path, "/web/reset_password")
            values = controller._prepare_signup_values(dict(base_qcontext))
        self.assertEqual(values.get("firstname"), "Reset")
        self.assertEqual(values.get("lastname"), "User")
        self.assertIn("name", values)
        # Same protection when the qcontext exposes the core reset flag.
        qcontext_flag = {
            key: value for key, value in base_qcontext.items() if key != "token"
        }
        qcontext_flag["reset_password"] = True
        with MockRequest(self.env, website=self.website, path="/web/signup"):
            values = controller._prepare_signup_values(qcontext_flag)
        self.assertEqual(values.get("firstname"), "Reset")

    # -- RF-08 -------------------------------------------------------------

    def test_08_backend_sale_order_with_membership_product_without_user(self):
        order = self.env["sale.order"].create({
            "partner_id": self.no_user_partner.id,
            "order_line": [(0, 0, {
                "product_id": self.membership_product.id,
                "product_uom_qty": 1.0,
            })],
        })
        self.assertTrue(order)
        self.assertEqual(order.order_line.product_id, self.membership_product)
        # Confirming the SO drives membership creation (core `membership`).
        order.action_confirm()
        self.assertEqual(order.state, "sale")

    # -- Phone not mandatory at checkout (EMES) ----------------------------

    def test_09_phone_not_mandatory_address_fields(self):
        """`phone` must not be part of the mandatory checkout fields, for
        any address (billing or delivery) and any flow on the EMES website.
        The core default (`website_sale/controllers/main.py:
        _get_mandatory_address_fields`) includes 'phone'; our override drops
        it. The frontend `address.js` then no longer marks the input
        `:required` nor shows the asterisk, and the server-side
        `_check_billing_address` / `_check_delivery_address` no longer
        reject empty phones.
        """
        country = self.env.ref("base.es")
        ctrl = WebsiteSaleMembershipSignup()
        fields = ctrl._get_mandatory_address_fields(country)
        self.assertNotIn("phone", fields)
        # Default mandatory fields still present.
        self.assertIn("name", fields)
        self.assertIn("street", fields)
        self.assertIn("city", fields)
        self.assertIn("country_id", fields)

    def test_10_phone_not_in_portal_mandatory_fields(self):
        """`phone` must also be dropped from the portal-side mandatory
        field list (`portal/controllers/portal.py:_get_mandatory_fields`
        returns `["name", "phone", "email", "street", "city",
        "country_id"]`). `website_sale`'s
        `_get_mandatory_billing_address_fields` UNIONS that set with the
        address set from `_get_mandatory_address_fields`, so even with the
        address override alone, 'phone' would come back via the portal
        path for billing. Hence this second override.
        """
        ctrl = WebsiteSaleMembershipSignup()
        fields = ctrl._get_mandatory_fields()
        self.assertNotIn("phone", fields)
        # Other portal mandatory fields still present.
        self.assertIn("name", fields)
        self.assertIn("email", fields)
        self.assertIn("country_id", fields)

    def test_11_phone_not_in_mandatory_billing_address_fields(self):
        """End-to-end check of the billing set: the union of
        `_get_mandatory_address_fields` + `_get_mandatory_fields` (which is
        what `_get_mandatory_billing_address_fields` returns) must NOT
        contain 'phone'.
        """
        country = self.env.ref("base.es")
        ctrl = WebsiteSaleMembershipSignup()
        billing_fields = ctrl._get_mandatory_billing_address_fields(country)
        self.assertNotIn("phone", billing_fields)
        self.assertIn("email", billing_fields)

    # -- Token-based signup: pre-fill firstname / lastname -----------------

    def test_12_signup_retrieve_info_returns_firstname_lastname(self):
        """When a partner is invited via token (the email-invitation flow),
        the core's `_signup_retrieve_info` only returns `name`. Our
        `res.partner` override adds `firstname` / `lastname` so the signup
        form (extended by this module to collect them separately) is
        pre-filled when the invited partner already has the split fields
        (which is the case with `partner_firstname` installed).

        The invited partner must NOT have a user yet, otherwise the token
        payload check (`user_ids`) won't match.
        """
        partner = self.env["res.partner"].create({
            "firstname": "Invited",
            "lastname": "Researcher",
            "email": "invited.researcher@example.com",
            "signup_type": "signup",
        })
        self.assertFalse(partner.user_ids, "test precondition: no user yet")
        token = partner._generate_signup_token()
        info = self.env["res.partner"]._signup_retrieve_info(token)
        self.assertEqual(info.get("firstname"), "Invited")
        self.assertEqual(info.get("lastname"), "Researcher")
        self.assertEqual(info.get("name"), partner.name)
        self.assertEqual(info.get("email"), "invited.researcher@example.com")

    def test_13_signup_retrieve_info_legacy_partner_without_split(self):
        """A partner created without `firstname`/`lastname` (legacy, only
        `name`) should not crash the override. `firstname`/`lastname` come
        back as '' (falsy), `name` as the legacy value, and the token flow
        still works — the form will show empty readonly inputs for the split
        fields, which is the best we can do without re-splitting.
        """
        partner = self.env["res.partner"].create({
            "name": "Legacy Researcher",
            "email": "legacy@example.com",
            "signup_type": "signup",
        })
        # partner_firstname's post_create hook may or may not have split
        # `name` automatically; we just check the override does not crash
        # and returns the contract keys.
        token = partner._generate_signup_token()
        info = self.env["res.partner"]._signup_retrieve_info(token)
        self.assertIn("firstname", info)
        self.assertIn("lastname", info)
        self.assertEqual(info.get("name"), partner.name)