from datetime import timedelta
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIClient

from shop_management.models.data.inventory import InventoryBatch, StockBalance, StockMovement
from shop_management.models.data.organization import Warehouse
from shop_management.models.data.product import Product
from shop_management.models.data.shop import Shop
from shop_management.models.data.utils import Currency
from shop_management.services.inventory import (
    allocate_fefo,
    calculate_weighted_average,
    issue_stock_fefo,
    receive_stock,
)


class InventoryCoreTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="inventory-user", password="test", is_staff=True
        )
        self.user.allowed_tabs = ["stock"]
        self.user.save(update_fields=["allowed_tabs"])
        self.shop = Shop.objects.create(owner=self.user, name="Kabul")
        self.warehouse = Warehouse.objects.create(
            shop=self.shop, code="MAIN", name="Main Warehouse"
        )
        self.currency = Currency.objects.create(shop=self.shop, title="AFN")
        self.product = Product.objects.create(
            shop=self.shop, name="Medicine", code="MED-1", requires_batch=True,
            requires_expiry=True,
        )
        self.today = timezone.localdate()

    def batch(self, number, days, blocked=False):
        return InventoryBatch.objects.create(
            product=self.product,
            batch_number=number,
            expiry_date=self.today + timedelta(days=days),
            is_blocked=blocked,
        )

    def receive(self, batch, quantity, cost):
        return receive_stock(
            warehouse=self.warehouse, product=self.product, batch=batch,
            quantity=quantity, unit_cost=cost, currency=self.currency,
            movement_type=StockMovement.MovementType.OPENING, created_by=self.user,
        )

    def test_weighted_average_formula(self):
        self.assertEqual(
            calculate_weighted_average("10", "100", "10", "200"),
            Decimal("150.000000"),
        )

    def test_receiving_updates_balance_and_creates_movement(self):
        batch = self.batch("B-1", 60)
        self.receive(batch, "10", "100")
        balance, _ = self.receive(batch, "10", "200")
        balance.refresh_from_db()
        self.assertEqual(balance.quantity, Decimal("20.0000"))
        self.assertEqual(balance.average_cost, Decimal("150.000000"))
        self.assertEqual(StockMovement.objects.count(), 2)

    def test_fefo_uses_earliest_expiry_and_splits_batches(self):
        later = self.batch("LATER", 90)
        earlier = self.batch("EARLIER", 30)
        self.receive(later, "10", "120")
        self.receive(earlier, "4", "100")
        allocations = allocate_fefo(self.warehouse, self.product, "7")
        self.assertEqual(
            [(row["batch"].batch_number, row["quantity"]) for row in allocations],
            [("EARLIER", Decimal("4.0000")), ("LATER", Decimal("3.0000"))],
        )

    def test_expired_and_blocked_stock_are_not_sellable(self):
        expired = self.batch("EXPIRED", -1)
        blocked = self.batch("BLOCKED", 30, blocked=True)
        self.receive(expired, "5", "10")
        self.receive(blocked, "5", "10")
        with self.assertRaises(ValidationError):
            allocate_fefo(self.warehouse, self.product, "1")

    def test_issue_prevents_negative_stock_and_creates_out_movement(self):
        batch = self.batch("SALE-BATCH", 30)
        self.receive(batch, "5", "10")
        allocations, movements = issue_stock_fefo(
            warehouse=self.warehouse, product=self.product, quantity="5",
            reference_type="Sale", reference_id="1", created_by=self.user,
        )
        balance = StockBalance.objects.get(batch=batch)
        self.assertEqual(balance.quantity, Decimal("0.0000"))
        self.assertEqual(movements[0].quantity_out, Decimal("5.0000"))
        with self.assertRaises(ValidationError):
            issue_stock_fefo(
                warehouse=self.warehouse, product=self.product, quantity="1"
            )

    def test_database_rejects_negative_controlled_stock(self):
        batch = self.batch("DATABASE-GUARD", 30)
        balance, _ = self.receive(batch, "2", "10")
        with self.assertRaises(IntegrityError), transaction.atomic():
            StockBalance.objects.filter(pk=balance.pk).update(quantity="-1")

    def test_stock_movement_is_immutable(self):
        batch = self.batch("IMMUTABLE", 30)
        _, movement = self.receive(batch, "1", "10")
        movement.remarks = "changed"
        with self.assertRaises(ValidationError):
            movement.save()
        with self.assertRaises(ValidationError):
            movement.delete()

    def test_balance_api_is_read_only(self):
        batch = self.batch("API", 30)
        balance, _ = self.receive(batch, "2", "10")
        client = APIClient()
        client.force_authenticate(self.user)
        response = client.patch(
            f"/api/shop/stock-balances/{balance.id}/", {"quantity": "999"}, format="json"
        )
        self.assertEqual(response.status_code, 405)
        balance.refresh_from_db()
        self.assertEqual(balance.quantity, Decimal("2.0000"))
