<?php

namespace Tests\Unit\Services;

use App\Models\Order;
use App\Models\Plan;
use App\Models\Product;
use App\Models\ProductFile;
use App\Models\PricingTier;
use App\Services\DownloadAccessService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;

class DownloadAccessServiceTest extends TestCase
{
    use RefreshDatabase;

    private DownloadAccessService $service;

    protected function setUp(): void
    {
        parent::setUp();
        $this->service = app(DownloadAccessService::class);
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    // ── Helpers ────────────────────────────────────────────────────────────────

    /**
     * A permissive ProductFile mock — no methods are called on it in these tests
     * because determineAccessVersion() returns early when supportPolicy is null.
     */
    private function makeFile(): MockInterface
    {
        return Mockery::mock(ProductFile::class)->shouldIgnoreMissing();
    }

    /**
     * Product mock. For bundles the main product only needs isBundle().
     * For included / regular products, files and supportPolicy are also accessed.
     */
    private function makeProduct(bool $isBundle, $files = null): MockInterface
    {
        $product = Mockery::mock(Product::class);
        $product->shouldReceive('isBundle')->andReturn($isBundle);
        $product->shouldReceive('getAttribute')->with('files')->andReturn($files ?? collect());
        $product->shouldReceive('getAttribute')->with('supportPolicy')->andReturn(null);

        return $product;
    }

    /**
     * Plan mock. billing_type = 1 (one-time) keeps the service out of the
     * subscription code path. bundleProducts is always declared so the mock
     * works for both bundle and non-bundle plans.
     */
    private function makePlan(array $bundleProducts = []): MockInterface
    {
        $plan = Mockery::mock(Plan::class);
        $plan->shouldReceive('getAttribute')->with('billing_type')->andReturn(1);
        $plan->shouldReceive('getAttribute')->with('bundleProducts')->andReturn(collect($bundleProducts));

        return $plan;
    }

    /**
     * PricingTier mock wired to the given plan and product.
     * byDefault() covers any other getAttribute calls the service makes.
     */
    private function makeTier(MockInterface $plan, MockInterface $product): MockInterface
    {
        $tier = Mockery::mock(PricingTier::class);
        $tier->shouldReceive('getAttribute')->with('plan')->andReturn($plan);
        $tier->shouldReceive('getAttribute')->with('product')->andReturn($product);
        $tier->shouldReceive('getAttribute')->andReturnNull()->byDefault();

        return $tier;
    }

    /**
     * Order mock that presents one OrderItem (via a plain stdClass so
     * Collection::where() can filter it by purchasable_type). The mock's id,
     * subscription_id, support_expires_at, and created_at are stubbed to keep
     * the service on the simple one-time, no-policy, no-license path.
     */
    private function makeOrder(MockInterface $tier): MockInterface
    {
        $item = (object) [
            'purchasable_type' => 'App\\Models\\PricingTier',
            'purchasable'      => $tier,
        ];

        $order = Mockery::mock(Order::class);
        $order->shouldReceive('getAttribute')->with('items')->andReturn(collect([$item]));
        $order->shouldReceive('getAttribute')->with('id')->andReturn(1);
        $order->shouldReceive('getAttribute')->with('subscription_id')->andReturn(null);
        $order->shouldReceive('getAttribute')->with('support_expires_at')->andReturn(null);
        $order->shouldReceive('getAttribute')->with('created_at')->andReturn(now());

        return $order;
    }

    // ── Tests ──────────────────────────────────────────────────────────────────

    public function test_non_bundle_product_produces_one_downloadable_item(): void
    {
        $product = $this->makeProduct(false, collect([$this->makeFile()]));
        $tier    = $this->makeTier($this->makePlan(), $product);
        $order   = $this->makeOrder($tier);

        $result = $this->service->mapOrdersToDownloadableItems(collect([$order]), 1);

        $this->assertCount(1, $result);
        $this->assertSame($product, $result->first()->product);
    }

    public function test_bundle_product_expands_into_one_item_per_included_product(): void
    {
        $included1 = $this->makeProduct(false, collect([$this->makeFile()]));
        $included2 = $this->makeProduct(false, collect([$this->makeFile()]));

        $plan  = $this->makePlan([$included1, $included2]);
        $tier  = $this->makeTier($plan, $this->makeProduct(true));
        $order = $this->makeOrder($tier);

        $result = $this->service->mapOrdersToDownloadableItems(collect([$order]), 1);

        $this->assertCount(2, $result);

        $returnedProducts = $result->values()->map(fn ($item) => $item->product);
        $this->assertTrue($returnedProducts->contains($included1));
        $this->assertTrue($returnedProducts->contains($included2));
    }

    public function test_bundle_skips_included_products_with_no_files(): void
    {
        $withFiles    = $this->makeProduct(false, collect([$this->makeFile()]));
        $withoutFiles = $this->makeProduct(false, collect());

        $plan  = $this->makePlan([$withFiles, $withoutFiles]);
        $tier  = $this->makeTier($plan, $this->makeProduct(true));
        $order = $this->makeOrder($tier);

        $result = $this->service->mapOrdersToDownloadableItems(collect([$order]), 1);

        $this->assertCount(1, $result);
        $this->assertSame($withFiles, $result->first()->product);
    }

    public function test_non_bundle_product_with_no_files_is_skipped(): void
    {
        $product = $this->makeProduct(false, collect()); // empty files
        $tier    = $this->makeTier($this->makePlan(), $product);
        $order   = $this->makeOrder($tier);

        $result = $this->service->mapOrdersToDownloadableItems(collect([$order]), 1);

        $this->assertCount(0, $result);
    }

    public function test_order_items_with_non_pricing_tier_purchasable_are_ignored(): void
    {
        $item = (object) [
            'purchasable_type' => 'App\\Models\\Addon',
            'purchasable'      => null,
        ];

        $order = Mockery::mock(Order::class);
        $order->shouldReceive('getAttribute')->with('items')->andReturn(collect([$item]));
        $order->shouldReceive('getAttribute')->with('created_at')->andReturn(now());

        $result = $this->service->mapOrdersToDownloadableItems(collect([$order]), 1);

        $this->assertCount(0, $result);
    }
}
