"""
inventory/tests/test_services.py
Tests for inventory service layer (stock movements, sales, payouts).
"""

from datetime import date
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.test import TestCase

from catalog_app.models import Author, Book
from inventory.models import (
    AuthorPayout,
    InventoryBalance,
    InventoryTransaction,
    Sale,
    SaleItem,
    StockLocation,
)
from inventory.services.inventory_service import (
    adjust_stock,
    receive_stock,
    transfer_stock,
)
from inventory.services.payout_service import (
    approve_payout,
    generate_payout,
    mark_payout_paid,
)
from inventory.services.sales_service import calculate_item_margins, record_sale

User = get_user_model()


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

def make_author(**kw):
    return Author.objects.create(
        name=kw.pop("name", "Test Author"),
        default_my_keep_percent=kw.pop("my_keep_percent", Decimal("25.00")),
        default_author_pay_percent=kw.pop("author_pay_percent", Decimal("75.00")),
        **kw,
    )


def make_book(author, **kw):
    return Book.objects.create(
        title=kw.pop("title", "Test Book"),
        author=author,
        price=kw.pop("price", Decimal("1500.00")),
        my_keep_percent=kw.pop("my_keep_percent", Decimal("25.00")),
        reorder_level=kw.pop("reorder_level", 5),
        track_inventory=True,
        is_listed=True,
        **kw,
    )


def make_location(code="WH001", **kw):
    return StockLocation.objects.create(
        name=kw.pop("name", "Warehouse"),
        code=code,
        location_type=kw.pop("location_type", StockLocation.TYPE_WAREHOUSE),
        **kw,
    )


def make_user(username="staff"):
    return User.objects.create_user(username=username, password="pass", is_staff=True)


# ---------------------------------------------------------------------------
# Stock movement tests
# ---------------------------------------------------------------------------

class ReceiveStockTest(TestCase):
    def setUp(self):
        self.author = make_author()
        self.book = make_book(self.author)
        self.location = make_location()
        self.user = make_user()

    def test_receive_creates_balance(self):
        receive_stock(book=self.book, location=self.location, quantity=10, user=self.user)
        balance = InventoryBalance.objects.get(book=self.book, location=self.location)
        self.assertEqual(balance.quantity_on_hand, 10)

    def test_receive_creates_transaction(self):
        receive_stock(book=self.book, location=self.location, quantity=10, user=self.user)
        tx = InventoryTransaction.objects.get(book=self.book, destination_location=self.location)
        self.assertEqual(tx.transaction_type, InventoryTransaction.TYPE_RECEIPT)
        self.assertEqual(tx.quantity, 10)

    def test_receive_accumulates(self):
        receive_stock(book=self.book, location=self.location, quantity=10, user=self.user)
        receive_stock(book=self.book, location=self.location, quantity=5, user=self.user)
        balance = InventoryBalance.objects.get(book=self.book, location=self.location)
        self.assertEqual(balance.quantity_on_hand, 15)

    def test_receive_rejects_zero(self):
        with self.assertRaises(ValueError):
            receive_stock(book=self.book, location=self.location, quantity=0, user=self.user)


class TransferStockTest(TestCase):
    def setUp(self):
        self.author = make_author()
        self.book = make_book(self.author)
        self.src = make_location(code="SRC")
        self.dst = make_location(code="DST")
        self.user = make_user()
        receive_stock(book=self.book, location=self.src, quantity=20, user=self.user)

    def test_transfer_moves_stock(self):
        transfer_stock(book=self.book, source=self.src, destination=self.dst, quantity=8, user=self.user)
        src_bal = InventoryBalance.objects.get(book=self.book, location=self.src)
        dst_bal = InventoryBalance.objects.get(book=self.book, location=self.dst)
        self.assertEqual(src_bal.quantity_on_hand, 12)
        self.assertEqual(dst_bal.quantity_on_hand, 8)

    def test_transfer_insufficient_stock_raises(self):
        with self.assertRaises(ValueError):
            transfer_stock(book=self.book, source=self.src, destination=self.dst, quantity=100, user=self.user)

    def test_transfer_same_location_raises(self):
        with self.assertRaises(ValueError):
            transfer_stock(book=self.book, source=self.src, destination=self.src, quantity=5, user=self.user)


class AdjustStockTest(TestCase):
    def setUp(self):
        self.author = make_author()
        self.book = make_book(self.author)
        self.location = make_location()
        self.user = make_user()
        receive_stock(book=self.book, location=self.location, quantity=10, user=self.user)

    def test_adjust_up(self):
        adjust_stock(book=self.book, location=self.location, new_quantity=15, user=self.user)
        balance = InventoryBalance.objects.get(book=self.book, location=self.location)
        self.assertEqual(balance.quantity_on_hand, 15)

    def test_adjust_down(self):
        adjust_stock(book=self.book, location=self.location, new_quantity=3, user=self.user)
        balance = InventoryBalance.objects.get(book=self.book, location=self.location)
        self.assertEqual(balance.quantity_on_hand, 3)

    def test_adjust_no_change_is_noop(self):
        initial_tx_count = InventoryTransaction.objects.count()
        adjust_stock(book=self.book, location=self.location, new_quantity=10, user=self.user)
        self.assertEqual(InventoryTransaction.objects.count(), initial_tx_count)


# ---------------------------------------------------------------------------
# Margin calculation tests (critical business logic)
# ---------------------------------------------------------------------------

class MarginCalculationTest(TestCase):
    def setUp(self):
        self.author = make_author()
        self.book = make_book(self.author, my_keep_percent=Decimal("25.00"))

    def test_no_discount(self):
        result = calculate_item_margins(
            book=self.book, quantity=1,
            unit_price=Decimal("1000.00"), discount_percent=Decimal("0"),
        )
        self.assertEqual(result["gross_amount"], Decimal("1000.00"))
        self.assertEqual(result["net_amount"], Decimal("1000.00"))
        self.assertEqual(result["my_profit_amount"], Decimal("250.00"))
        self.assertEqual(result["author_payout_amount"], Decimal("750.00"))

    def test_discount_applied_before_split(self):
        # 10% discount on 1000 → net 900
        # 25% of 900 = 225 platform; 75% of 900 = 675 author
        result = calculate_item_margins(
            book=self.book, quantity=1,
            unit_price=Decimal("1000.00"), discount_percent=Decimal("10"),
        )
        self.assertEqual(result["net_amount"], Decimal("900.00"))
        self.assertEqual(result["my_profit_amount"], Decimal("225.00"))
        self.assertEqual(result["author_payout_amount"], Decimal("675.00"))

    def test_quantity_multiplied(self):
        result = calculate_item_margins(
            book=self.book, quantity=3,
            unit_price=Decimal("1000.00"), discount_percent=Decimal("0"),
        )
        self.assertEqual(result["gross_amount"], Decimal("3000.00"))
        self.assertEqual(result["my_profit_amount"], Decimal("750.00"))

    def test_margins_sum_to_net(self):
        result = calculate_item_margins(
            book=self.book, quantity=2,
            unit_price=Decimal("750.00"), discount_percent=Decimal("15"),
        )
        self.assertAlmostEqual(
            float(result["my_profit_amount"] + result["author_payout_amount"]),
            float(result["net_amount"]),
            places=2,
        )


# ---------------------------------------------------------------------------
# Record sale tests
# ---------------------------------------------------------------------------

class RecordSaleTest(TestCase):
    def setUp(self):
        self.author = make_author()
        self.book = make_book(self.author)
        self.location = make_location()
        self.user = make_user()
        receive_stock(book=self.book, location=self.location, quantity=50, user=self.user)

    def test_record_sale_creates_sale_and_items(self):
        sale = record_sale(
            sale_channel=Sale.CHANNEL_DIRECT,
            sale_date=date.today(),
            items=[{
                "book": self.book,
                "quantity": 3,
                "unit_price": Decimal("1500.00"),
                "discount_percent": Decimal("0"),
                "stock_location": self.location,
            }],
            user=self.user,
        )
        self.assertIsNotNone(sale.pk)
        self.assertEqual(sale.items.count(), 1)

    def test_record_sale_deducts_stock(self):
        record_sale(
            sale_channel=Sale.CHANNEL_DIRECT,
            sale_date=date.today(),
            items=[{
                "book": self.book,
                "quantity": 5,
                "unit_price": Decimal("1500.00"),
                "discount_percent": Decimal("0"),
                "stock_location": self.location,
            }],
            user=self.user,
        )
        balance = InventoryBalance.objects.get(book=self.book, location=self.location)
        self.assertEqual(balance.quantity_on_hand, 45)

    def test_record_sale_insufficient_stock_raises(self):
        with self.assertRaises(ValueError):
            record_sale(
                sale_channel=Sale.CHANNEL_DIRECT,
                sale_date=date.today(),
                items=[{
                    "book": self.book,
                    "quantity": 999,
                    "unit_price": Decimal("1500.00"),
                    "discount_percent": Decimal("0"),
                    "stock_location": self.location,
                }],
                user=self.user,
            )

    def test_sale_totals_calculated_correctly(self):
        sale = record_sale(
            sale_channel=Sale.CHANNEL_DIRECT,
            sale_date=date.today(),
            items=[{
                "book": self.book,
                "quantity": 2,
                "unit_price": Decimal("1000.00"),
                "discount_percent": Decimal("10"),
                "stock_location": self.location,
            }],
            user=self.user,
        )
        # 2 × 1000 = 2000 gross; 10% discount = 200; net = 1800
        self.assertEqual(sale.total_gross, Decimal("2000.00"))
        self.assertEqual(sale.total_discount, Decimal("200.00"))
        self.assertEqual(sale.total_net, Decimal("1800.00"))


# ---------------------------------------------------------------------------
# Payout service tests
# ---------------------------------------------------------------------------

class PayoutServiceTest(TestCase):
    def setUp(self):
        self.author = make_author()
        self.book = make_book(self.author)
        self.location = make_location()
        self.user = make_user()
        receive_stock(book=self.book, location=self.location, quantity=50, user=self.user)
        self.sale = record_sale(
            sale_channel=Sale.CHANNEL_DIRECT,
            sale_date=date(2025, 1, 15),
            items=[{
                "book": self.book,
                "quantity": 2,
                "unit_price": Decimal("1000.00"),
                "discount_percent": Decimal("0"),
                "stock_location": self.location,
            }],
            user=self.user,
        )

    def test_generate_payout(self):
        payout = generate_payout(
            author=self.author,
            period_type=AuthorPayout.PERIOD_MONTHLY,
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            user=self.user,
        )
        self.assertEqual(payout.status, AuthorPayout.STATUS_PENDING)
        self.assertGreater(payout.total_earnings, Decimal("0"))

    def test_approve_payout_marks_items(self):
        payout = generate_payout(
            author=self.author,
            period_type=AuthorPayout.PERIOD_MONTHLY,
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            user=self.user,
        )
        approver = make_user("approver")
        approve_payout(payout=payout, approver=approver)
        payout.refresh_from_db()
        self.assertEqual(payout.status, AuthorPayout.STATUS_APPROVED)
        # SaleItems should be locked
        self.assertTrue(
            self.sale.items.filter(payout_included=True).exists()
        )

    def test_double_generate_excludes_paid_items(self):
        payout1 = generate_payout(
            author=self.author,
            period_type=AuthorPayout.PERIOD_MONTHLY,
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            user=self.user,
        )
        approve_payout(payout=payout1, approver=self.user)
        # Second generate should return empty (no new items)
        payout2 = generate_payout(
            author=self.author,
            period_type=AuthorPayout.PERIOD_MONTHLY,
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            user=self.user,
        )
        self.assertEqual(payout2.total_earnings, Decimal("0"))

    def test_mark_payout_paid(self):
        payout = generate_payout(
            author=self.author,
            period_type=AuthorPayout.PERIOD_MONTHLY,
            period_start=date(2025, 1, 1),
            period_end=date(2025, 1, 31),
            user=self.user,
        )
        approve_payout(payout=payout, approver=self.user)
        mark_payout_paid(
            payout=payout,
            payment_method="M-Pesa",
            payment_reference="QWE12345",
            payment_date=date(2025, 2, 1),
            user=self.user,
        )
        payout.refresh_from_db()
        self.assertEqual(payout.status, AuthorPayout.STATUS_PAID)
        self.assertEqual(payout.payment_reference, "QWE12345")
