# tests/unit/services/test_inventory_service.py
"""Unit tests for InventoryService."""

import uuid

import pytest

from app.exceptions import (
    InsufficientInventoryException,
    InvalidQuantityException,
    InventoryNotFoundException,
    InventoryValidationException,
    ProductNotFoundException,
)
from app.services.inventory_service import InventoryService
from models.database.inventory import Inventory
from models.schema.inventory import (
    InventoryAdjust,
    InventoryCreate,
    InventoryReserve,
    InventoryUpdate,
)


@pytest.mark.unit
@pytest.mark.inventory
class TestInventoryService:
    """Test suite for InventoryService."""

    def setup_method(self):
        """Initialize service instance before each test."""
        self.service = InventoryService()

    # ==================== Validation Helper Tests ====================

    def test_validate_quantity_positive(self):
        """Test that positive quantity passes validation."""
        # Should not raise
        self.service._validate_quantity(10, allow_zero=True)
        self.service._validate_quantity(10, allow_zero=False)

    def test_validate_quantity_zero_allowed(self):
        """Test that zero quantity passes when allow_zero=True."""
        # Should not raise
        self.service._validate_quantity(0, allow_zero=True)

    def test_validate_quantity_zero_not_allowed(self):
        """Test that zero quantity fails when allow_zero=False."""
        with pytest.raises(InvalidQuantityException) as exc_info:
            self.service._validate_quantity(0, allow_zero=False)

        assert exc_info.value.error_code == "INVALID_QUANTITY"
        assert "must be positive" in str(exc_info.value)

    def test_validate_quantity_negative(self):
        """Test that negative quantity fails validation."""
        with pytest.raises(InvalidQuantityException) as exc_info:
            self.service._validate_quantity(-5, allow_zero=True)

        assert exc_info.value.error_code == "INVALID_QUANTITY"
        assert "cannot be negative" in str(exc_info.value)

    def test_validate_quantity_none(self):
        """Test that None quantity fails validation."""
        with pytest.raises(InvalidQuantityException) as exc_info:
            self.service._validate_quantity(None, allow_zero=True)

        assert exc_info.value.error_code == "INVALID_QUANTITY"
        assert "required" in str(exc_info.value)

    def test_validate_location_valid(self):
        """Test valid location normalization."""
        result = self.service._validate_location("warehouse_a")
        assert result == "WAREHOUSE_A"

    def test_validate_location_with_whitespace(self):
        """Test location normalization strips whitespace."""
        result = self.service._validate_location("  warehouse_a  ")
        assert result == "WAREHOUSE_A"

    def test_validate_location_empty(self):
        """Test empty location fails validation."""
        with pytest.raises(InventoryValidationException) as exc_info:
            self.service._validate_location("")

        assert "required" in str(exc_info.value)

    def test_validate_location_whitespace_only(self):
        """Test whitespace-only location fails validation."""
        with pytest.raises(InventoryValidationException) as exc_info:
            self.service._validate_location("   ")

        assert "required" in str(exc_info.value)

    # ==================== Set Inventory Tests ====================

    def test_set_inventory_new_entry_success(self, db, test_product, test_vendor):
        """Test setting inventory for a new product/location combination."""
        unique_id = str(uuid.uuid4())[:8].upper()
        inventory_data = InventoryCreate(
            product_id=test_product.id,
            location=f"WAREHOUSE_NEW_{unique_id}",
            quantity=100,
        )

        result = self.service.set_inventory(db, test_vendor.id, inventory_data)

        assert result.product_id == test_product.id
        assert result.vendor_id == test_vendor.id
        assert result.location == f"WAREHOUSE_NEW_{unique_id}"
        assert result.quantity == 100

    def test_set_inventory_existing_entry_replaces(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test setting inventory replaces existing quantity."""
        inventory_data = InventoryCreate(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=200,
        )

        result = self.service.set_inventory(db, test_vendor.id, inventory_data)

        assert result.quantity == 200  # Replaced, not added

    def test_set_inventory_product_not_found(self, db, test_vendor):
        """Test setting inventory for non-existent product raises exception."""
        from app.exceptions.base import ValidationException

        unique_id = str(uuid.uuid4())[:8].upper()
        inventory_data = InventoryCreate(
            product_id=99999,  # Non-existent product
            location=f"WAREHOUSE_{unique_id}",
            quantity=100,
        )

        # Service wraps ProductNotFoundException in ValidationException
        with pytest.raises((ProductNotFoundException, ValidationException)):
            self.service.set_inventory(db, test_vendor.id, inventory_data)

    def test_set_inventory_zero_quantity(self, db, test_product, test_vendor):
        """Test setting inventory with zero quantity succeeds."""
        unique_id = str(uuid.uuid4())[:8].upper()
        inventory_data = InventoryCreate(
            product_id=test_product.id,
            location=f"WAREHOUSE_ZERO_{unique_id}",
            quantity=0,
        )

        result = self.service.set_inventory(db, test_vendor.id, inventory_data)

        assert result.quantity == 0

    # ==================== Adjust Inventory Tests ====================

    def test_adjust_inventory_add_new_entry(self, db, test_product, test_vendor):
        """Test adjusting inventory creates new entry with positive quantity."""
        unique_id = str(uuid.uuid4())[:8].upper()
        inventory_data = InventoryAdjust(
            product_id=test_product.id,
            location=f"WAREHOUSE_ADJUST_{unique_id}",
            quantity=50,
        )

        result = self.service.adjust_inventory(db, test_vendor.id, inventory_data)

        assert result.quantity == 50

    def test_adjust_inventory_add_to_existing(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test adjusting inventory adds to existing quantity."""
        original_quantity = test_inventory.quantity

        inventory_data = InventoryAdjust(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=25,
        )

        result = self.service.adjust_inventory(db, test_vendor.id, inventory_data)

        assert result.quantity == original_quantity + 25

    def test_adjust_inventory_remove_from_existing(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test adjusting inventory removes from existing quantity."""
        original_quantity = test_inventory.quantity

        inventory_data = InventoryAdjust(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=-10,
        )

        result = self.service.adjust_inventory(db, test_vendor.id, inventory_data)

        assert result.quantity == original_quantity - 10

    def test_adjust_inventory_remove_insufficient(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test removing more than available raises exception."""
        from app.exceptions.base import ValidationException

        inventory_data = InventoryAdjust(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=-(test_inventory.quantity + 100),  # More than available
        )

        # Service wraps InsufficientInventoryException in ValidationException
        with pytest.raises((InsufficientInventoryException, ValidationException)):
            self.service.adjust_inventory(db, test_vendor.id, inventory_data)

    def test_adjust_inventory_remove_nonexistent(self, db, test_product, test_vendor):
        """Test removing from non-existent inventory raises InventoryNotFoundException."""
        unique_id = str(uuid.uuid4())[:8].upper()
        inventory_data = InventoryAdjust(
            product_id=test_product.id,
            location=f"NONEXISTENT_{unique_id}",
            quantity=-10,
        )

        with pytest.raises(InventoryNotFoundException):
            self.service.adjust_inventory(db, test_vendor.id, inventory_data)

    # ==================== Reserve Inventory Tests ====================

    def test_reserve_inventory_success(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test reserving inventory succeeds."""
        original_reserved = test_inventory.reserved_quantity
        available = test_inventory.quantity - test_inventory.reserved_quantity
        reserve_qty = min(10, available)

        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=reserve_qty,
        )

        result = self.service.reserve_inventory(db, test_vendor.id, reserve_data)

        assert result.reserved_quantity == original_reserved + reserve_qty

    def test_reserve_inventory_insufficient_available(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test reserving more than available raises exception."""
        from app.exceptions.base import ValidationException

        available = test_inventory.quantity - test_inventory.reserved_quantity

        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=available + 100,  # More than available
        )

        # Service wraps InsufficientInventoryException in ValidationException
        with pytest.raises((InsufficientInventoryException, ValidationException)):
            self.service.reserve_inventory(db, test_vendor.id, reserve_data)

    def test_reserve_inventory_not_found(self, db, test_product, test_vendor):
        """Test reserving non-existent inventory raises InventoryNotFoundException."""
        unique_id = str(uuid.uuid4())[:8].upper()
        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=f"NONEXISTENT_{unique_id}",
            quantity=10,
        )

        with pytest.raises(InventoryNotFoundException):
            self.service.reserve_inventory(db, test_vendor.id, reserve_data)

    # ==================== Release Reservation Tests ====================

    def test_release_reservation_success(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test releasing reservation succeeds."""
        original_reserved = test_inventory.reserved_quantity
        release_qty = min(5, original_reserved)

        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=release_qty,
        )

        result = self.service.release_reservation(db, test_vendor.id, reserve_data)

        assert result.reserved_quantity == original_reserved - release_qty

    def test_release_reservation_more_than_reserved(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test releasing more than reserved sets to zero (doesn't error)."""
        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=test_inventory.reserved_quantity + 100,
        )

        result = self.service.release_reservation(db, test_vendor.id, reserve_data)

        assert result.reserved_quantity == 0

    # ==================== Fulfill Reservation Tests ====================

    def test_fulfill_reservation_success(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test fulfilling reservation decreases both quantity and reserved."""
        original_quantity = test_inventory.quantity
        original_reserved = test_inventory.reserved_quantity
        fulfill_qty = min(5, original_reserved, original_quantity)

        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=fulfill_qty,
        )

        result = self.service.fulfill_reservation(db, test_vendor.id, reserve_data)

        assert result.quantity == original_quantity - fulfill_qty
        assert result.reserved_quantity == max(0, original_reserved - fulfill_qty)

    def test_fulfill_reservation_insufficient_inventory(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test fulfilling more than quantity raises exception."""
        from app.exceptions.base import ValidationException

        reserve_data = InventoryReserve(
            product_id=test_product.id,
            location=test_inventory.location,
            quantity=test_inventory.quantity + 100,
        )

        # Service wraps InsufficientInventoryException in ValidationException
        with pytest.raises((InsufficientInventoryException, ValidationException)):
            self.service.fulfill_reservation(db, test_vendor.id, reserve_data)

    # ==================== Get Product Inventory Tests ====================

    def test_get_product_inventory_success(
        self, db, test_inventory, test_product, test_vendor
    ):
        """Test getting product inventory summary."""
        result = self.service.get_product_inventory(db, test_vendor.id, test_product.id)

        assert result.product_id == test_product.id
        assert result.vendor_id == test_vendor.id
        assert result.total_quantity >= test_inventory.quantity
        assert len(result.locations) >= 1

    def test_get_product_inventory_no_inventory(self, db, test_product, test_vendor):
        """Test getting inventory for product with no inventory entries."""
        # Create a new product without inventory
        from models.database.marketplace_product import MarketplaceProduct
        from models.database.marketplace_product_translation import (
            MarketplaceProductTranslation,
        )
        from models.database.product import Product

        unique_id = str(uuid.uuid4())[:8]
        mp = MarketplaceProduct(
            marketplace_product_id=f"MP_EMPTY_{unique_id}",
            price="29.99",
            marketplace="Letzshop",
        )
        db.add(mp)
        db.flush()

        # Create translation for the product (title is now in translations table)
        translation = MarketplaceProductTranslation(
            marketplace_product_id=mp.id,
            language="en",
            title="Product Without Inventory",
        )
        db.add(translation)
        db.commit()

        product = Product(
            vendor_id=test_vendor.id,
            marketplace_product_id=mp.id,
            is_active=True,
        )
        db.add(product)
        db.commit()

        result = self.service.get_product_inventory(db, test_vendor.id, product.id)

        assert result.total_quantity == 0
        assert result.total_reserved == 0
        assert len(result.locations) == 0

    def test_get_product_inventory_not_found(self, db, test_vendor):
        """Test getting inventory for non-existent product raises exception."""
        from app.exceptions.base import ValidationException

        # Service wraps ProductNotFoundException in ValidationException
        with pytest.raises((ProductNotFoundException, ValidationException)):
            self.service.get_product_inventory(db, test_vendor.id, 99999)

    # ==================== Get Vendor Inventory Tests ====================

    def test_get_vendor_inventory_success(self, db, test_inventory, test_vendor):
        """Test getting all vendor inventory."""
        result = self.service.get_vendor_inventory(db, test_vendor.id)

        assert len(result) >= 1
        assert any(inv.id == test_inventory.id for inv in result)

    def test_get_vendor_inventory_with_location_filter(
        self, db, test_inventory, test_vendor
    ):
        """Test getting vendor inventory filtered by location."""
        result = self.service.get_vendor_inventory(
            db, test_vendor.id, location=test_inventory.location[:10]
        )

        assert len(result) >= 1
        for inv in result:
            assert test_inventory.location[:10].upper() in inv.location.upper()

    def test_get_vendor_inventory_with_low_stock_filter(self, db, test_vendor):
        """Test getting vendor inventory filtered by low stock threshold."""
        result = self.service.get_vendor_inventory(
            db, test_vendor.id, low_stock_threshold=5
        )

        for inv in result:
            assert inv.quantity <= 5

    def test_get_vendor_inventory_pagination(self, db, test_vendor):
        """Test vendor inventory pagination."""
        result = self.service.get_vendor_inventory(db, test_vendor.id, skip=0, limit=10)

        assert len(result) <= 10

    # ==================== Update Inventory Tests ====================

    def test_update_inventory_quantity(self, db, test_inventory, test_vendor):
        """Test updating inventory quantity."""
        inventory_update = InventoryUpdate(quantity=500)

        result = self.service.update_inventory(
            db, test_vendor.id, test_inventory.id, inventory_update
        )

        assert result.quantity == 500

    def test_update_inventory_reserved_quantity(self, db, test_inventory, test_vendor):
        """Test updating inventory reserved quantity."""
        inventory_update = InventoryUpdate(reserved_quantity=20)

        result = self.service.update_inventory(
            db, test_vendor.id, test_inventory.id, inventory_update
        )

        assert result.reserved_quantity == 20

    def test_update_inventory_location(self, db, test_inventory, test_vendor):
        """Test updating inventory location."""
        unique_id = str(uuid.uuid4())[:8].upper()
        new_location = f"NEW_LOCATION_{unique_id}"
        inventory_update = InventoryUpdate(location=new_location)

        result = self.service.update_inventory(
            db, test_vendor.id, test_inventory.id, inventory_update
        )

        assert result.location == new_location.upper()

    def test_update_inventory_not_found(self, db, test_vendor):
        """Test updating non-existent inventory raises InventoryNotFoundException."""
        inventory_update = InventoryUpdate(quantity=100)

        with pytest.raises(InventoryNotFoundException):
            self.service.update_inventory(db, test_vendor.id, 99999, inventory_update)

    def test_update_inventory_wrong_vendor(self, db, test_inventory, other_company):
        """Test updating inventory from wrong vendor raises InventoryNotFoundException."""
        from models.database.vendor import Vendor

        unique_id = str(uuid.uuid4())[:8]
        other_vendor = Vendor(
            company_id=other_company.id,
            vendor_code=f"OTHER_{unique_id.upper()}",
            subdomain=f"other{unique_id.lower()}",
            name=f"Other Vendor {unique_id}",
            is_active=True,
        )
        db.add(other_vendor)
        db.commit()

        inventory_update = InventoryUpdate(quantity=100)

        with pytest.raises(InventoryNotFoundException):
            self.service.update_inventory(
                db, other_vendor.id, test_inventory.id, inventory_update
            )

    # ==================== Delete Inventory Tests ====================

    def test_delete_inventory_success(self, db, test_inventory, test_vendor):
        """Test deleting inventory entry."""
        inventory_id = test_inventory.id

        result = self.service.delete_inventory(db, test_vendor.id, inventory_id)

        assert result is True

        # Verify deleted
        deleted = db.query(Inventory).filter(Inventory.id == inventory_id).first()
        assert deleted is None

    def test_delete_inventory_not_found(self, db, test_vendor):
        """Test deleting non-existent inventory raises InventoryNotFoundException."""
        with pytest.raises(InventoryNotFoundException):
            self.service.delete_inventory(db, test_vendor.id, 99999)

    def test_delete_inventory_wrong_vendor(self, db, test_inventory, other_company):
        """Test deleting inventory from wrong vendor raises InventoryNotFoundException."""
        from models.database.vendor import Vendor

        unique_id = str(uuid.uuid4())[:8]
        other_vendor = Vendor(
            company_id=other_company.id,
            vendor_code=f"DELOTHER_{unique_id.upper()}",
            subdomain=f"delother{unique_id.lower()}",
            name=f"Delete Other Vendor {unique_id}",
            is_active=True,
        )
        db.add(other_vendor)
        db.commit()

        with pytest.raises(InventoryNotFoundException):
            self.service.delete_inventory(db, other_vendor.id, test_inventory.id)
