from datetime import timedelta
from decimal import Decimal
from unittest.mock import patch

from django.contrib.admin.sites import AdminSite
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import ProtectedError
from django.test import RequestFactory, TestCase
from django.utils import timezone

from authentication.models import User
from club.models import Club, ClubMembership, Season
from club.tenancy import reset_current_club, set_current_club
from members.models import Member
from teams.models import Position, Team

from .admin import ProductAdmin
from .models import (
    AppliedDiscount,
    Cart,
    CartItem,
    Discount,
    DiscountType,
    Invoice,
    Order,
    OrderLine,
    Payment,
    Product,
)


class ProductSlugTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")

    def test_slug_auto_populated_from_name(self):
        product = Product.objects.create(club=self.club, name="Home Jersey")

        self.assertEqual(product.slug, "home-jersey")

    def test_explicit_slug_is_preserved(self):
        product = Product.objects.create(club=self.club, name="Home Jersey", slug="custom")

        self.assertEqual(product.slug, "custom")

    def test_slug_is_unique_per_club_with_suffix(self):
        first = Product.objects.create(club=self.club, name="Home Jersey")
        second = Product.objects.create(club=self.club, name="Home Jersey")

        self.assertEqual(first.slug, "home-jersey")
        self.assertEqual(second.slug, "home-jersey-2")

    def test_same_slug_allowed_in_a_different_club(self):
        other = Club.objects.create(name="Rival FC", slug="rival-fc")
        here = Product.objects.create(club=self.club, name="Home Jersey")
        there = Product.objects.create(club=other, name="Home Jersey")

        self.assertEqual(here.slug, there.slug)

    def test_unsluggable_name_falls_back(self):
        product = Product.objects.create(club=self.club, name="###")

        self.assertEqual(product.slug, "item")

    def test_str_returns_name(self):
        product = Product.objects.create(club=self.club, name="Home Jersey")

        self.assertEqual(str(product), "Home Jersey")


class OpenCartConstraintTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
        cls.user = User.objects.create_user(email="shopper@example.com", password="pw")

    def test_only_one_open_cart_per_user_per_club(self):
        Cart.objects.create(club=self.club, user=self.user)

        with self.assertRaises(IntegrityError):
            Cart.objects.create(club=self.club, user=self.user)

    def test_open_and_closed_carts_coexist(self):
        Cart.objects.create(club=self.club, user=self.user, status=Cart.CartStatus.CHECKED_OUT)
        Cart.objects.create(club=self.club, user=self.user, status=Cart.CartStatus.ABANDONED)
        Cart.objects.create(club=self.club, user=self.user)

        self.assertEqual(self.user.carts.count(), 3)
        self.assertEqual(self.user.carts.filter(status=Cart.CartStatus.OPEN).count(), 1)

    def test_open_cart_allowed_in_each_club(self):
        other = Club.objects.create(name="Rival FC", slug="rival-fc")
        Cart.objects.create(club=self.club, user=self.user)
        Cart.objects.create(club=other, user=self.user)

        self.assertEqual(self.user.carts.filter(status=Cart.CartStatus.OPEN).count(), 2)

    def test_str(self):
        cart = Cart.objects.create(club=self.club, user=self.user)

        self.assertEqual(str(cart), f"{self.user} - open")


class CartItemTests(TestCase):
    def test_str(self):
        club = Club.objects.create(name="Ajax United", slug="ajax-united")
        user = User.objects.create_user(email="shopper@example.com", password="pw")
        cart = Cart.objects.create(club=club, user=user)
        product = Product.objects.create(club=club, name="Home Jersey")
        item = CartItem.objects.create(cart=cart, product=product, quantity=2, unit_price=Decimal("25.00"))

        self.assertEqual(str(item), "Home Jersey - 2x")


class OrderNumberTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
        cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
        cls.year = timezone.now().year

    def make_order(self, **kwargs):
        kwargs.setdefault("club", self.club)
        kwargs.setdefault("purchaser", self.member)
        kwargs.setdefault("total", Decimal("10.00"))
        return Order.objects.create(**kwargs)

    def test_number_is_generated(self):
        order = self.make_order()

        self.assertEqual(order.number, f"ORD-{self.year}-00001")

    def test_number_increments_within_club_and_year(self):
        first = self.make_order()
        second = self.make_order()

        self.assertEqual(first.number, f"ORD-{self.year}-00001")
        self.assertEqual(second.number, f"ORD-{self.year}-00002")

    def test_number_is_scoped_per_club(self):
        other = Club.objects.create(name="Rival FC", slug="rival-fc")
        self.make_order()

        order = self.make_order(club=other)

        self.assertEqual(order.number, f"ORD-{self.year}-00001")

    def test_explicit_number_is_preserved(self):
        order = self.make_order(number="CUSTOM-1")

        self.assertEqual(order.number, "CUSTOM-1")

    def test_resaving_keeps_the_number(self):
        order = self.make_order()
        original = order.number

        order.status = Order.OrderStatus.PAID
        order.save()

        order.refresh_from_db()
        self.assertEqual(order.number, original)

    def test_non_numeric_suffix_is_ignored(self):
        Order.objects.create(club=self.club, purchaser=self.member, total=Decimal("5"), number=f"ORD-{self.year}-oops")

        order = self.make_order()

        self.assertEqual(order.number, f"ORD-{self.year}-00001")

    def test_club_filled_from_tenant_context(self):
        token = set_current_club(self.club)
        try:
            order = Order.objects.create(purchaser=self.member, total=Decimal("5"))
        finally:
            reset_current_club(token)

        self.assertEqual(order.club, self.club)

    def test_retries_on_collision(self):
        taken = self.make_order().number
        with patch.object(Order, "generate_number", side_effect=[taken, "ORD-2999-00001"]):
            order = Order(club=self.club, purchaser=self.member, total=Decimal("5"))
            order.save()

        self.assertEqual(order.number, "ORD-2999-00001")

    def test_gives_up_after_exhausting_retries(self):
        taken = self.make_order().number
        with patch.object(Order, "generate_number", return_value=taken), self.assertRaises(IntegrityError):
            Order(club=self.club, purchaser=self.member, total=Decimal("5")).save()

    def test_str_is_the_number(self):
        order = self.make_order()

        self.assertEqual(str(order), order.number)


class ShopEntitiesTestBase(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
        cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
        cls.product = Product.objects.create(club=cls.club, name="Home Jersey")
        cls.order = Order.objects.create(club=cls.club, purchaser=cls.member, total=Decimal("50.00"))
        cls.year = timezone.now().year


class OrderLineTests(ShopEntitiesTestBase):
    def make_line(self, **kwargs):
        kwargs.setdefault("order", self.order)
        kwargs.setdefault("product", self.product)
        kwargs.setdefault("quantity", 2)
        kwargs.setdefault("unit_price", Decimal("25.00"))
        kwargs.setdefault("line_total", Decimal("50.00"))
        return OrderLine.objects.create(**kwargs)

    def test_str(self):
        self.assertEqual(str(self.make_line()), "Home Jersey - 2x")

    def test_deleting_order_cascades_to_lines(self):
        self.make_line()
        self.order.delete()
        self.assertFalse(OrderLine.objects.exists())

    def test_product_is_protected_while_referenced(self):
        self.make_line()
        with self.assertRaises(ProtectedError):
            self.product.delete()


class DiscountTests(ShopEntitiesTestBase):
    def test_slug_and_str(self):
        discount = Discount.objects.create(club=self.club, name="Sibling discount")

        self.assertEqual(discount.slug, "sibling-discount")
        self.assertEqual(str(discount), "Sibling discount")

    def test_slug_is_unique_per_club(self):
        Discount.objects.create(club=self.club, name="Sibling")
        second = Discount.objects.create(club=self.club, name="Sibling")

        self.assertEqual(second.slug, "sibling-2")


class AppliedDiscountTests(ShopEntitiesTestBase):
    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()
        cls.discount = Discount.objects.create(club=cls.club, name="Sibling")

    def apply(self, **kwargs):
        kwargs.setdefault("order", self.order)
        kwargs.setdefault("discount", self.discount)
        kwargs.setdefault("discount_amount", Decimal("10.00"))
        return AppliedDiscount.objects.create(**kwargs)

    def test_str_percentage_shows_percent(self):
        applied = self.apply(discount_type=DiscountType.PERCENTAGE)

        self.assertEqual(str(applied), "Sibling - 10.00%")

    def test_str_fixed_amount_has_no_percent(self):
        applied = self.apply(discount_type=DiscountType.FIXED_AMOUNT)

        self.assertEqual(str(applied), "Sibling - 10.00")

    def test_deleting_order_cascades(self):
        self.apply()
        self.order.delete()
        self.assertFalse(AppliedDiscount.objects.exists())

    def test_discount_is_protected_while_referenced(self):
        self.apply()
        with self.assertRaises(ProtectedError):
            self.discount.delete()


class PaymentTests(ShopEntitiesTestBase):
    def test_str(self):
        payment = Payment.objects.create(order=self.order, amount=Decimal("50.00"))

        self.assertEqual(str(payment), f"{self.order} - pending")

    def test_deleting_order_cascades(self):
        Payment.objects.create(order=self.order, amount=Decimal("50.00"))
        self.order.delete()
        self.assertFalse(Payment.objects.exists())


class InvoiceTests(ShopEntitiesTestBase):
    def test_number_generated_and_str(self):
        invoice = Invoice.objects.create(club=self.club, order=self.order)

        self.assertEqual(invoice.number, f"INV-{self.year}-00001")
        self.assertEqual(str(invoice), invoice.number)

    def test_number_increments_per_club(self):
        second_order = Order.objects.create(club=self.club, purchaser=self.member, total=Decimal("10.00"))
        Invoice.objects.create(club=self.club, order=self.order)
        second = Invoice.objects.create(club=self.club, order=second_order)

        self.assertEqual(second.number, f"INV-{self.year}-00002")

    def test_one_invoice_per_order(self):
        Invoice.objects.create(club=self.club, order=self.order)

        with self.assertRaises(IntegrityError):
            Invoice.objects.create(club=self.club, order=self.order)

    def test_deleting_order_cascades_to_invoice(self):
        Invoice.objects.create(club=self.club, order=self.order)
        self.order.delete()
        self.assertFalse(Invoice.objects.exists())


class ClubScopeValidationTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
        cls.other = Club.objects.create(name="Rival FC", slug="rival-fc")
        today = timezone.localdate()
        cls.season = Season.objects.create(club=cls.club, start_date=today, end_date=today + timedelta(days=300))
        cls.other_season = Season.objects.create(club=cls.other, start_date=today, end_date=today + timedelta(days=300))
        cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
        ClubMembership.objects.create(club=cls.club, member=cls.member, season=cls.season)
        cls.stranger = Member.objects.create(first_name="Stray", last_name="Ger")

    def make_cart(self, club):
        user = User.objects.create_user(email=f"u-{club.slug}@example.com", password="pw")
        return Cart.objects.create(club=club, user=user)

    def make_order(self, club):
        return Order.objects.create(club=club, purchaser=self.member, total=Decimal("10.00"))

    # --- Product ---
    def test_product_rejects_cross_club_season(self):
        product = Product(club=self.club, name="Jersey", season=self.other_season)
        with self.assertRaises(ValidationError) as ctx:
            product.full_clean()
        self.assertIn("season", ctx.exception.error_dict)

    def test_product_rejects_cross_club_staff_role(self):
        position = Position.objects.create(club=self.other, name="Coach", short_name="C", staff_position=True)
        product = Product(club=self.club, name="Jersey", staff_role=position)
        with self.assertRaises(ValidationError) as ctx:
            product.full_clean()
        self.assertIn("staff_role", ctx.exception.error_dict)

    def test_product_accepts_same_club_season(self):
        Product(club=self.club, name="Jersey", season=self.season).full_clean()

    # --- CartItem ---
    def test_cartitem_rejects_cross_club_product(self):
        cart = self.make_cart(self.club)
        product = Product.objects.create(club=self.other, name="Jersey")
        item = CartItem(cart=cart, product=product, unit_price=Decimal("5"))
        with self.assertRaises(ValidationError) as ctx:
            item.full_clean()
        self.assertIn("product", ctx.exception.error_dict)

    def test_cartitem_rejects_cross_club_team(self):
        cart = self.make_cart(self.club)
        product = Product.objects.create(club=self.club, name="Jersey")
        team = Team.objects.create(club=self.other, name="First", short_name="1")
        item = CartItem(cart=cart, product=product, team=team, unit_price=Decimal("5"))
        with self.assertRaises(ValidationError) as ctx:
            item.full_clean()
        self.assertIn("team", ctx.exception.error_dict)

    def test_cartitem_rejects_non_member_beneficiary(self):
        cart = self.make_cart(self.club)
        product = Product.objects.create(club=self.club, name="Jersey")
        item = CartItem(cart=cart, product=product, beneficiary=self.stranger, unit_price=Decimal("5"))
        with self.assertRaises(ValidationError) as ctx:
            item.full_clean()
        self.assertIn("beneficiary", ctx.exception.error_dict)

    def test_cartitem_accepts_same_club(self):
        cart = self.make_cart(self.club)
        product = Product.objects.create(club=self.club, name="Jersey")
        CartItem(cart=cart, product=product, beneficiary=self.member, unit_price=Decimal("5")).full_clean()

    def test_cartitem_clean_without_cart_is_noop(self):
        CartItem().clean()

    # --- OrderLine ---
    def test_orderline_rejects_cross_club_product(self):
        order = self.make_order(self.club)
        product = Product.objects.create(club=self.other, name="Jersey")
        line = OrderLine(order=order, product=product, unit_price=Decimal("5"), line_total=Decimal("5"))
        with self.assertRaises(ValidationError) as ctx:
            line.full_clean()
        self.assertIn("product", ctx.exception.error_dict)

    def test_orderline_clean_without_order_is_noop(self):
        OrderLine().clean()

    # --- Order ---
    def test_order_rejects_non_member_purchaser(self):
        order = Order(club=self.club, purchaser=self.stranger, total=Decimal("10"))
        with self.assertRaises(ValidationError) as ctx:
            order.full_clean()
        self.assertIn("purchaser", ctx.exception.error_dict)

    def test_order_accepts_member_purchaser(self):
        Order(club=self.club, purchaser=self.member, total=Decimal("10")).full_clean()

    # --- AppliedDiscount ---
    def test_applieddiscount_rejects_cross_club_discount(self):
        order = self.make_order(self.club)
        discount = Discount.objects.create(club=self.other, name="Sibling")
        applied = AppliedDiscount(order=order, discount=discount, discount_amount=Decimal("5"))
        with self.assertRaises(ValidationError) as ctx:
            applied.full_clean()
        self.assertIn("discount", ctx.exception.error_dict)

    def test_applieddiscount_rejects_non_member_applied_by(self):
        order = self.make_order(self.club)
        discount = Discount.objects.create(club=self.club, name="Sibling")
        applied = AppliedDiscount(order=order, discount=discount, discount_amount=Decimal("5"), applied_by=self.stranger)
        with self.assertRaises(ValidationError) as ctx:
            applied.full_clean()
        self.assertIn("applied_by", ctx.exception.error_dict)

    def test_applieddiscount_is_unique_per_order(self):
        order = self.make_order(self.club)
        discount = Discount.objects.create(club=self.club, name="Sibling")
        AppliedDiscount.objects.create(order=order, discount=discount, discount_amount=Decimal("5"))
        with self.assertRaises(IntegrityError):
            AppliedDiscount.objects.create(order=order, discount=discount, discount_amount=Decimal("5"))

    def test_applieddiscount_clean_without_order_is_noop(self):
        AppliedDiscount().clean()

    # --- Invoice ---
    def test_invoice_rejects_cross_club_order(self):
        order = self.make_order(self.other)
        invoice = Invoice(club=self.club, order=order)
        with self.assertRaises(ValidationError) as ctx:
            invoice.full_clean()
        self.assertIn("order", ctx.exception.error_dict)

    def test_invoice_accepts_same_club_order(self):
        order = self.make_order(self.club)
        Invoice(club=self.club, order=order).full_clean()


class AdminScopingTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
        cls.other = Club.objects.create(name="Rival FC", slug="rival-fc")
        today = timezone.localdate()
        cls.season = Season.objects.create(club=cls.club, start_date=today, end_date=today + timedelta(days=300))
        cls.other_season = Season.objects.create(club=cls.other, start_date=today, end_date=today + timedelta(days=300))

    def test_fk_dropdown_scoped_to_object_club(self):
        product = Product.objects.create(club=self.club, name="Jersey")
        admin_obj = ProductAdmin(Product, AdminSite())
        request = RequestFactory().get("/")
        request._club_obj = product

        field = admin_obj.formfield_for_foreignkey(Product._meta.get_field("season"), request)

        self.assertIn(self.season, field.queryset)
        self.assertNotIn(self.other_season, field.queryset)
