# Copyright 2025 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from datetime import date

from freezegun import freeze_time

from psycopg2 import ProgrammingError

from odoo import fields
from odoo.tests import Form, common


@freeze_time("2026-01-15")
class TestMembershipPaymentDate(common.TransactionCase):
    """Test the membership_payment_date module."""

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # Workaround: website_sale adds base_unit_count as a compute+store+required
        # field on product.template with NOT NULL constraint but without precompute.
        # The ORM doesn't include it in INSERT for computed stored fields with
        # inverse setter, so we set a DB default to avoid the NOT NULL violation.
        # This column only exists if website_sale is installed, so we make it
        # conditional to allow running tests without that dependency.
        for table in ("product_template", "product_product"):
            try:
                cls.env.cr.execute(
                    "ALTER TABLE %s "
                    "ALTER COLUMN base_unit_count SET DEFAULT 0" % table
                )
            except ProgrammingError:
                cls.env.cr.rollback()
        # Accounts
        cls.account_receivable = cls.env["account.account"].create(
            {
                "name": "Test Receivable",
                "code": "RECV",
                "account_type": "asset_receivable",
                "reconcile": True,
            }
        )
        cls.account_income = cls.env["account.account"].create(
            {
                "name": "Test Income",
                "code": "INCM",
                "account_type": "income",
            }
        )
        # Journals
        cls.sale_journal = cls.env["account.journal"].create(
            {
                "name": "Test Sale Journal",
                "code": "TSJ",
                "type": "sale",
                "default_account_id": cls.account_income.id,
            }
        )
        cls.bank_journal = cls.env["account.journal"].create(
            {
                "name": "Test Bank Journal",
                "code": "TBJ",
                "type": "bank",
            }
        )
        # Partner
        cls.partner = cls.env["res.partner"].create(
            {
                "name": "Test Member",
                "property_account_receivable_id": cls.account_receivable.id,
            }
        )
        # Fixed membership product (full year 2026)
        template_fixed = cls.env["product.template"].create(
            {
                "name": "Fixed Membership 2026",
                "type": "service",
                "membership": True,
                "membership_type": "fixed",
                "membership_date_from": date(2026, 1, 1),
                "membership_date_to": date(2026, 12, 31),
                "list_price": 100.0,
            }
        )
        cls.product_fixed = template_fixed.product_variant_id
        # Fixed membership product for 2027 (renewal scenario)
        template_fixed_2027 = cls.env["product.template"].create(
            {
                "name": "Fixed Membership 2027",
                "type": "service",
                "membership": True,
                "membership_type": "fixed",
                "membership_date_from": date(2027, 1, 1),
                "membership_date_to": date(2027, 12, 31),
                "list_price": 100.0,
            }
        )
        cls.product_fixed_2027 = template_fixed_2027.product_variant_id
        # Variable membership product (1 year)
        template_variable = cls.env["product.template"].create(
            {
                "name": "Variable Membership",
                "type": "service",
                "membership": True,
                "membership_type": "variable",
                "membership_interval_qty": 1,
                "membership_interval_unit": "years",
                "list_price": 200.0,
            }
        )
        cls.product_variable = template_variable.product_variant_id

    def _create_invoice(self, product, invoice_date, quantity=1.0):
        """Create and return a draft out_invoice with the given product."""
        invoice_form = Form(
            self.env["account.move"].with_context(
                default_move_type="out_invoice"
            )
        )
        invoice_form.invoice_date = invoice_date
        invoice_form.partner_id = self.partner
        with invoice_form.invoice_line_ids.new() as line_form:
            line_form.product_id = product
            line_form.price_unit = product.list_price
            line_form.quantity = quantity
        return invoice_form.save()

    def _post_invoice(self, invoice):
        """Post the invoice and return the membership line if any."""
        invoice.action_post()
        membership_lines = self.env["membership.membership_line"].search(
            [
                ("account_invoice_line", "in", invoice.invoice_line_ids.ids),
            ]
        )
        return membership_lines

    def _register_payment(self, invoice, payment_date):
        """Register a payment for the invoice on the given date."""
        payment_register = (
            self.env["account.payment.register"]
            .with_context(
                active_model="account.move",
                active_ids=invoice.ids,
            )
            .create(
                {
                    "amount": invoice.amount_total,
                    "payment_date": payment_date,
                    "journal_id": self.bank_journal.id,
                }
            )
        )
        payment_register._create_payments()

    def _create_active_membership(self, product, date_from, date_to, state="paid"):
        """Create a membership line for the test partner."""
        return self.env["membership.membership_line"].create(
            {
                "partner": self.partner.id,
                "membership_id": product.id,
                "member_price": product.list_price,
                "date": date_from,
                "date_from": date_from,
                "date_to": date_to,
                "state": state,
            }
        )

    # --- Tests ---

    def test_01_new_membership_fixed_paid(self):
        """New member, fixed product, pay after invoice — date_from = payment_date."""
        invoice = self._create_invoice(
            self.product_fixed, invoice_date=date(2026, 1, 1)
        )
        mlines = self._post_invoice(invoice)
        self.assertEqual(len(mlines), 1)
        mline = mlines[0]
        # Check initial state
        self.assertEqual(mline.state, "invoiced")
        # Register payment on 2026-03-15
        self._register_payment(invoice, payment_date=date(2026, 3, 15))
        # Re-read to get updated values
        mline = self.env["membership.membership_line"].browse(mline.id)
        # Verify dates corrected
        self.assertEqual(mline.date_from, date(2026, 3, 15))
        self.assertEqual(mline.date_to, date(2026, 12, 31))

    def test_02_new_membership_variable_paid(self):
        """New member, variable product, pay after invoice — correct date_to."""
        invoice = self._create_invoice(
            self.product_variable, invoice_date=date(2026, 1, 1)
        )
        mlines = self._post_invoice(invoice)
        self.assertEqual(len(mlines), 1)
        mline = mlines[0]
        # Register payment 6 months later
        self._register_payment(invoice, payment_date=date(2026, 6, 15))
        # date_from = payment_date, date_to = payment_date + 1 year - 1 day
        self.assertEqual(mline.date_from, date(2026, 6, 15))
        self.assertEqual(mline.date_to, date(2027, 6, 14))

    def test_03_renewal_before_expiry_fixed(self):
        """Active member renews early — new membership starts after expiry."""
        # Create active membership until 2026-12-31
        self._create_active_membership(
            self.product_fixed,
            date_from=date(2026, 1, 1),
            date_to=date(2026, 12, 31),
            state="paid",
        )
        # Buy a new fixed 2027 product and pay on 2026-11-15
        invoice = self._create_invoice(
            self.product_fixed_2027, invoice_date=date(2026, 11, 1)
        )
        mlines = self._post_invoice(invoice)
        self.assertEqual(len(mlines), 1)
        mline = mlines[0]
        self._register_payment(invoice, payment_date=date(2026, 11, 15))
        # Must start the day after the active membership expires (no overlap)
        self.assertEqual(mline.date_from, date(2027, 1, 1))
        self.assertEqual(mline.date_to, date(2027, 12, 31))

    def test_04_renewal_before_expiry_variable(self):
        """Active member renews early with variable product."""
        self._create_active_membership(
            self.product_variable,
            date_from=date(2026, 1, 1),
            date_to=date(2026, 12, 31),
            state="paid",
        )
        invoice = self._create_invoice(
            self.product_variable, invoice_date=date(2026, 11, 1)
        )
        mlines = self._post_invoice(invoice)
        self.assertEqual(len(mlines), 1)
        mline = mlines[0]
        self._register_payment(invoice, payment_date=date(2026, 11, 15))
        # Must start the day after expiry
        self.assertEqual(mline.date_from, date(2027, 1, 1))
        # 1-year variable from 2027-01-01 -> ends 2027-12-31
        self.assertEqual(mline.date_to, date(2027, 12, 31))

    def test_05_renewal_expired_no_overlap(self):
        """Member with expired membership — uses payment_date (no active)."""
        # Expired membership (date_to < today)
        self._create_active_membership(
            self.product_fixed,
            date_from=date(2025, 1, 1),
            date_to=date(2025, 12, 31),
            state="paid",
        )
        invoice = self._create_invoice(
            self.product_fixed, invoice_date=date(2026, 3, 1)
        )
        mlines = self._post_invoice(invoice)
        mline = mlines[0]
        self._register_payment(invoice, payment_date=date(2026, 3, 15))
        # No active membership -> date_from = payment_date
        self.assertEqual(mline.date_from, date(2026, 3, 15))
        self.assertEqual(mline.date_to, date(2026, 12, 31))

    def test_06_bank_transfer_flow(self):
        """Simulate full bank transfer flow: invoice -> post -> pay."""
        invoice = self._create_invoice(
            self.product_fixed, invoice_date=date(2026, 2, 1)
        )
        mlines = self._post_invoice(invoice)
        self.assertEqual(len(mlines), 1)
        mline = mlines[0]
        # After posting, state must be "invoiced"
        self.assertEqual(mline.state, "invoiced")
        # Register payment
        self._register_payment(invoice, payment_date=date(2026, 2, 20))
        # State should become "paid" automatically
        self.assertEqual(mline.state, "paid")
        # Dates must be corrected
        self.assertEqual(mline.date_from, date(2026, 2, 20))
        self.assertEqual(mline.date_to, date(2026, 12, 31))

    def test_07_renewal_same_product(self):
        """Renew the same product type — no overlap with existing active membership."""
        # Active membership on product_fixed covers 2026
        self._create_active_membership(
            self.product_fixed,
            date_from=date(2026, 1, 1),
            date_to=date(2026, 12, 31),
            state="paid",
        )
        # Same conceptual product but for next year
        invoice = self._create_invoice(
            self.product_fixed_2027, invoice_date=date(2026, 6, 1)
        )
        mlines = self._post_invoice(invoice)
        self.assertEqual(len(mlines), 1)
        mline = mlines[0]
        self._register_payment(invoice, payment_date=date(2026, 6, 15))
        # New line starts after the active one expires, no overlap
        self.assertEqual(mline.date_from, date(2027, 1, 1))
        self.assertEqual(mline.date_to, date(2027, 12, 31))

    def test_08_no_active_membership_fallback(self):
        """Partner with no memberships at all — date_from = payment_date."""
        invoice = self._create_invoice(
            self.product_fixed, invoice_date=date(2026, 5, 1)
        )
        mlines = self._post_invoice(invoice)
        mline = mlines[0]
        self._register_payment(invoice, payment_date=date(2026, 5, 10))
        self.assertEqual(mline.date_from, date(2026, 5, 10))
        self.assertEqual(mline.date_to, date(2026, 12, 31))
