<?php

namespace Tests\Unit;

use App\Exceptions\InsufficientStockException;
use App\Exceptions\StockException;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\StockMovement;
use App\Models\StockReservation;
use App\Models\User;
use App\Models\VariantStockMovement;
use App\Services\InventoryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class InventoryServiceTest extends TestCase
{
    use RefreshDatabase;

    protected InventoryService $inventoryService;

    protected function setUp(): void
    {
        parent::setUp();
        $this->inventoryService = new InventoryService();
    }

    public function test_check_availability_returns_true_when_stock_sufficient(): void
    {
        $product = Product::factory()->create(['stock' => 10]);

        $result = $this->inventoryService->checkAvailability($product->id, 5);

        $this->assertTrue($result);
    }

    public function test_check_availability_returns_false_when_stock_insufficient(): void
    {
        $product = Product::factory()->create(['stock' => 5]);

        $result = $this->inventoryService->checkAvailability($product->id, 10);

        $this->assertFalse($result);
    }

    public function test_reserve_stock_creates_reservation(): void
    {
        $product = Product::factory()->create(['stock' => 10]);

        $token = $this->inventoryService->reserve($product->id, 5);

        $this->assertNotEmpty($token);
        $this->assertDatabaseHas('stock_reservations', [
            'product_id' => $product->id,
            'quantity' => 5,
            'status' => 'pending',
        ]);
    }

    public function test_reserve_stock_throws_exception_when_insufficient(): void
    {
        $product = Product::factory()->create(['stock' => 5]);

        $this->expectException(InsufficientStockException::class);

        $this->inventoryService->reserve($product->id, 10);
    }

    public function test_confirm_reservation_decrements_stock(): void
    {
        $product = Product::factory()->create(['stock' => 10]);
        $token = $this->inventoryService->reserve($product->id, 5);

        $this->inventoryService->confirmReservation($token);

        $product->refresh();
        $this->assertEquals(5, $product->stock);
        $this->assertDatabaseHas('stock_reservations', [
            'reservation_token' => $token,
            'status' => 'confirmed',
        ]);
    }

    public function test_release_reservation_updates_status(): void
    {
        $product = Product::factory()->create(['stock' => 10]);
        $token = $this->inventoryService->reserve($product->id, 5);

        $this->inventoryService->release($token);

        $this->assertDatabaseHas('stock_reservations', [
            'reservation_token' => $token,
            'status' => 'released',
        ]);
    }

    public function test_get_available_stock_considers_reservations(): void
    {
        $product = Product::factory()->create(['stock' => 10]);
        
        // Reserve 5 units
        $this->inventoryService->reserve($product->id, 5);

        // Available should be 5 (10 total - 5 reserved)
        $available = $this->inventoryService->getAvailableStock($product->id);
        
        $this->assertEquals(5, $available);
    }

    public function test_decrement_stock_with_lock(): void
    {
        $product = Product::factory()->create(['stock' => 10]);

        $result = $this->inventoryService->decrement($product->id, 5);

        $this->assertTrue($result);
        $product->refresh();
        $this->assertEquals(5, $product->stock);
    }

    public function test_decrement_stock_throws_exception_when_insufficient(): void
    {
        $product = Product::factory()->create(['stock' => 5]);

        $this->expectException(InsufficientStockException::class);

        $this->inventoryService->decrement($product->id, 10);
    }

    public function test_handle_order_status_change_restock_on_delivered_cancelled(): void
    {
        [$product, $order] = $this->createOrderWithSaleMovement(Order::STATUS_DELIVERED, 3);

        $this->inventoryService->handleOrderStatusChange(
            $order->fresh('items'),
            Order::STATUS_DELIVERED,
            Order::STATUS_CANCELLED
        );

        $product->refresh();
        $this->assertSame(3, $product->current_stock);
        $this->assertDatabaseHas('product_stock_movements', [
            'product_id' => $product->id,
            'type' => StockMovement::TYPE_RESTOCK,
            'quantity' => 3,
            'notes' => "Order #{$order->id} cancelled",
        ]);
    }

    public function test_restock_order_is_idempotent(): void
    {
        [$product, $order] = $this->createOrderWithSaleMovement(Order::STATUS_DELIVERED, 2);

        $this->inventoryService->restockOrder($order->fresh('items'), 'cancelled');
        $this->inventoryService->restockOrder($order->fresh('items'), 'cancelled');

        $restockCount = StockMovement::where('product_id', $product->id)
            ->where('type', StockMovement::TYPE_RESTOCK)
            ->where('notes', "Order #{$order->id} cancelled")
            ->count();

        $this->assertSame(1, $restockCount);
        $product->refresh();
        $this->assertSame(2, $product->current_stock);
    }

    public function test_handle_order_status_change_releases_reservation_on_awaiting_payment_cancel(): void
    {
        $product = Product::factory()->create([
            'product_type' => 'single',
            'current_stock' => 10,
        ]);
        $user = User::create([
            'name' => 'Customer',
            'mobile' => '9000000099',
            'email' => 'cust99@example.com',
            'password' => bcrypt('password'),
            'user_type' => 'customer',
        ]);

        $order = Order::create([
            'user_id' => $user->id,
            'source' => 'online',
            'address_snapshot' => ['name' => 'Test'],
            'subtotal' => 100,
            'discount_total' => 0,
            'tax_total' => 0,
            'shipping_total' => 0,
            'total_payable' => 100,
            'payment_status' => Order::PAYMENT_PENDING,
            'payment_method' => 'online',
            'status' => Order::STATUS_AWAITING_PAYMENT,
        ]);

        $reservation = StockReservation::create([
            'product_id' => $product->id,
            'order_id' => $order->id,
            'quantity' => 2,
            'reservation_token' => 'token-abc',
            'expires_at' => now()->addMinutes(10),
            'status' => 'pending',
        ]);

        $this->inventoryService->handleOrderStatusChange(
            $order,
            Order::STATUS_AWAITING_PAYMENT,
            Order::STATUS_CANCELLED
        );

        $reservation->refresh();
        $this->assertSame('released', $reservation->status);
        $this->assertDatabaseMissing('product_stock_movements', [
            'product_id' => $product->id,
            'notes' => "Order #{$order->id} cancelled",
        ]);
    }

    public function test_handle_order_status_change_restock_on_shipped_returned(): void
    {
        [$product, $order] = $this->createOrderWithSaleMovement(Order::STATUS_SHIPPED, 4);

        $this->inventoryService->handleOrderStatusChange(
            $order->fresh('items'),
            Order::STATUS_SHIPPED,
            Order::STATUS_RETURNED
        );

        $product->refresh();
        $this->assertSame(4, $product->current_stock);
        $this->assertDatabaseHas('product_stock_movements', [
            'product_id' => $product->id,
            'type' => StockMovement::TYPE_RESTOCK,
            'quantity' => 4,
            'notes' => "Order #{$order->id} returned",
        ]);
    }

    public function test_restock_order_finds_pos_sale_in_variant_movements_table(): void
    {
        $parent = Product::factory()->create([
            'product_type' => 'variant',
            'current_stock' => 0,
        ]);
        $product = Product::factory()->create([
            'product_type' => 'variant',
            'parent_id' => $parent->id,
            'current_stock' => 0,
        ]);
        $user = User::create([
            'name' => 'Customer',
            'mobile' => '9000000100',
            'email' => 'cust100@example.com',
            'password' => bcrypt('password'),
            'user_type' => 'customer',
        ]);

        $order = Order::create([
            'user_id' => $user->id,
            'source' => 'pos',
            'address_snapshot' => ['name' => 'POS'],
            'subtotal' => 500,
            'discount_total' => 0,
            'tax_total' => 0,
            'shipping_total' => 0,
            'total_payable' => 500,
            'payment_status' => Order::PAYMENT_PAID,
            'payment_method' => 'cash',
            'status' => Order::STATUS_DELIVERED,
            'placed_at' => now(),
        ]);

        OrderItem::create([
            'order_id' => $order->id,
            'product_id' => $product->id,
            'name' => $product->name,
            'sku' => 'SKU-POS',
            'qty' => 5,
            'price' => 100,
            'line_total' => 500,
        ]);

        VariantStockMovement::create([
            'product_id' => $product->id,
            'type' => VariantStockMovement::TYPE_SALE,
            'quantity' => -5,
            'notes' => "POS Order #{$order->id}",
            'created_by' => 1,
        ]);
        $product->recalculateStock();

        $this->inventoryService->restockOrder($order->fresh('items'), 'cancelled');

        $product->refresh();
        $this->assertSame(5, $product->current_stock);

        $this->assertDatabaseHas('variant_stock_movements', [
            'product_id' => $product->id,
            'type' => VariantStockMovement::TYPE_RESTOCK,
            'quantity' => 5,
            'notes' => "Order #{$order->id} cancelled",
        ]);
    }

    /**
     * @return array{0: Product, 1: Order}
     */
    private function createOrderWithSaleMovement(string $status, int $qty): array
    {
        $product = Product::factory()->create([
            'product_type' => 'single',
            'current_stock' => 0,
        ]);
        $user = User::create([
            'name' => 'Customer',
            'mobile' => '9000000' . random_int(100, 999),
            'email' => 'cust' . random_int(1000, 9999) . '@example.com',
            'password' => bcrypt('password'),
            'user_type' => 'customer',
        ]);

        $order = Order::create([
            'user_id' => $user->id,
            'source' => 'online',
            'address_snapshot' => ['name' => 'Test'],
            'subtotal' => 1000,
            'discount_total' => 0,
            'tax_total' => 0,
            'shipping_total' => 0,
            'total_payable' => 1000,
            'payment_status' => Order::PAYMENT_PAID,
            'payment_method' => 'online',
            'status' => $status,
            'placed_at' => now(),
        ]);

        OrderItem::create([
            'order_id' => $order->id,
            'product_id' => $product->id,
            'name' => $product->name,
            'sku' => 'SKU-1',
            'qty' => $qty,
            'price' => 1000 / max($qty, 1),
            'line_total' => 1000,
        ]);

        StockMovement::create([
            'product_id' => $product->id,
            'type' => StockMovement::TYPE_SALE,
            'quantity' => -$qty,
            'notes' => "Order #{$order->id}",
            'created_by' => 1,
        ]);
        $product->recalculateStock();

        return [$product, $order];
    }
}

