<?php

namespace Tests\Unit;

use App\Exceptions\CarrierCreateFailedException;
use App\Models\Carrier;
use App\Models\Order;
use App\Models\Shipment;
use App\Models\User;
use App\Services\OrderTrackingNotificationService;
use App\Services\Shipment\ShipmentBookingService;
use App\Services\Shipment\ShipmentCarrierFallbackService;
use App\Services\Shipment\ShipmentStatusSyncService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Mockery;
use Tests\TestCase;

class ShipmentBookingServiceTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        $this->createMinimalSchema();
    }

    public function test_falls_back_to_ekart_when_delhivery_fails(): void
    {
        [$order, $delhivery, $ekart] = $this->makePackedOrderWithCarriers();

        $shipment = Shipment::create([
            'order_id' => $order->id,
            'carrier_id' => $delhivery->id,
            'status' => Shipment::STATUS_PENDING,
        ]);

        $sync = Mockery::mock(ShipmentStatusSyncService::class);
        $sync->shouldReceive('applyCourierStatus')->once()->andReturnUsing(function (Shipment $createdShipment) {
            $createdShipment->order->update(['status' => Order::STATUS_AWAITING_PICKUP]);
        });

        $service = $this->makeBookingService($sync);
        $service->book($shipment, [], function (Carrier $carrier) {
            if ($carrier->slug === 'delhivery') {
                throw new CarrierCreateFailedException(
                    'Prepaid client manifest charge API failed due to insufficient balance',
                    0,
                    null,
                    ['remarks' => ['insufficient balance']],
                    CarrierCreateFailedException::FAILURE_INSUFFICIENT_BALANCE
                );
            }

            return [
                'tracking_id' => 'EKARTAWB1',
                'label_url' => 'https://example.test/label.pdf',
                'raw_response' => ['ok' => true],
                'metadata' => [],
            ];
        });

        $shipment->refresh();
        $order->refresh();

        $this->assertSame(Shipment::STATUS_CREATED, $shipment->status);
        $this->assertSame($ekart->id, $shipment->carrier_id);
        $this->assertSame('EKARTAWB1', $shipment->tracking_id);
        $this->assertSame('ekart', $shipment->metadata['fallback']['to'] ?? null);
        $this->assertSame(Order::STATUS_AWAITING_PICKUP, $order->status);
    }

    public function test_both_carriers_fail_without_changing_order_status(): void
    {
        [$order, $delhivery] = $this->makePackedOrderWithCarriers();

        $shipment = Shipment::create([
            'order_id' => $order->id,
            'carrier_id' => $delhivery->id,
            'status' => Shipment::STATUS_PENDING,
        ]);

        $sync = Mockery::mock(ShipmentStatusSyncService::class);
        $sync->shouldReceive('applyCourierStatus')->never();

        $service = $this->makeBookingService($sync);

        try {
            $service->book($shipment, [], function (Carrier $carrier) {
                $reason = $carrier->slug === 'delhivery'
                    ? CarrierCreateFailedException::FAILURE_INSUFFICIENT_BALANCE
                    : CarrierCreateFailedException::FAILURE_NON_SERVICEABLE_PINCODE;

                throw new CarrierCreateFailedException(
                    $carrier->slug . ' failed',
                    0,
                    null,
                    ['carrier' => $carrier->slug],
                    $reason
                );
            });
            $this->fail('Expected CarrierCreateFailedException was not thrown.');
        } catch (CarrierCreateFailedException $e) {
            $this->assertGreaterThanOrEqual(2, count($e->getRawResponse()['attempts'] ?? []));
        }

        $shipment->refresh();
        $order->refresh();

        $this->assertSame(Shipment::STATUS_FAILED, $shipment->status);
        $this->assertSame(Order::STATUS_PACKED, $order->status);
    }

    private function makeBookingService(ShipmentStatusSyncService $sync): ShipmentBookingService
    {
        $notifier = Mockery::mock(OrderTrackingNotificationService::class);
        $notifier->shouldIgnoreMissing();

        return new ShipmentBookingService(
            new ShipmentCarrierFallbackService(),
            $sync,
            $notifier
        );
    }

    /**
     * @return array{0: Order, 1: Carrier, 2: Carrier}
     */
    private function makePackedOrderWithCarriers(): array
    {
        $user = User::create([
            'name' => 'Customer',
            'mobile' => '9100000001',
            'email' => 'customer@example.com',
            'password' => bcrypt('password'),
            'user_type' => 'customer',
        ]);

        $delhivery = $this->makeCarrier('delhivery', 'Delhivery');
        $ekart = $this->makeCarrier('ekart', 'Ekart');

        $order = Order::create([
            'user_id' => $user->id,
            'source' => 'online',
            'address_snapshot' => ['name' => 'Test', 'pin' => '641604'],
            'subtotal' => 1000,
            'discount_total' => 0,
            'tax_total' => 0,
            'shipping_total' => 0,
            'total_payable' => 1000,
            'payment_status' => Order::PAYMENT_PAID,
            'payment_method' => 'online',
            'status' => Order::STATUS_PACKED,
            'placed_at' => now(),
        ]);

        return [$order, $delhivery, $ekart];
    }

    private function makeCarrier(string $slug, string $name): Carrier
    {
        $carrier = new Carrier();
        $carrier->name = $name;
        $carrier->slug = $slug;
        $carrier->enabled = true;
        $carrier->process_type = Carrier::PROCESS_TYPE_AUTOMATIC;
        $carrier->api_keys = 'test-keys';
        $carrier->save();

        return $carrier;
    }

    private function createMinimalSchema(): void
    {
        Schema::dropIfExists('shipments');
        Schema::dropIfExists('orders');
        Schema::dropIfExists('carriers');
        Schema::dropIfExists('users');

        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('mobile')->nullable();
            $table->string('email')->nullable();
            $table->string('password')->nullable();
            $table->string('user_type')->nullable();
            $table->timestamps();
        });

        Schema::create('carriers', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug');
            $table->boolean('enabled')->default(true);
            $table->string('process_type')->default('automatic');
            $table->text('api_keys')->nullable();
            $table->json('config_schema')->nullable();
            $table->timestamps();
        });

        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id')->nullable();
            $table->string('source')->nullable();
            $table->json('address_snapshot')->nullable();
            $table->decimal('subtotal', 10, 2)->default(0);
            $table->decimal('discount_total', 10, 2)->default(0);
            $table->decimal('tax_total', 10, 2)->default(0);
            $table->decimal('shipping_total', 10, 2)->default(0);
            $table->decimal('total_payable', 10, 2)->default(0);
            $table->string('payment_status')->nullable();
            $table->string('payment_method')->nullable();
            $table->string('status')->nullable();
            $table->string('delhivery_awb')->nullable();
            $table->string('delhivery_label_url')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();
        });

        Schema::create('shipments', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('order_id');
            $table->unsignedBigInteger('carrier_id');
            $table->string('tracking_id')->nullable();
            $table->string('status')->nullable();
            $table->string('label_url')->nullable();
            $table->json('raw_response')->nullable();
            $table->json('metadata')->nullable();
            $table->timestamps();
        });
    }
}
