<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Domain\Blog\AccessDecision;
use App\Domain\Blog\ArticleAccess;
use App\Domain\Plugin\Hook;
use App\Domain\Plugin\PluginManager;
use App\Models\Article;
use App\Models\Category;
use App\Models\Plugin;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Validation\ValidationException;
use Plugins\Larablog\Membership\Database\Seeders\MembershipPlanSeeder;
use Plugins\Larablog\Membership\Domain\MembershipService;
use Plugins\Larablog\Membership\Models\ArticleMembership;
use Plugins\Larablog\Membership\Models\MembershipPlan;
use Plugins\Larablog\Membership\PluginServiceProvider as MembershipProvider;
use Plugins\Larablog\PaidContent\PluginServiceProvider as PaidContentProvider;
use Plugins\Larablog\Payment\Domain\OrderService;
use Plugins\Larablog\Payment\Domain\ProductType;
use Plugins\Larablog\Payment\Models\Entitlement;
use Plugins\Larablog\Payment\PluginServiceProvider as PaymentProvider;
use RuntimeException;
use Tests\TestCase;

class MembershipCommerceTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        Hook::flush();

        $this->artisan('migrate', ['--path' => 'plugins/larablog/payment/database/migrations']);
        $this->artisan('migrate', ['--path' => 'plugins/larablog/paid-content/database/migrations']);
        $this->artisan('migrate', ['--path' => 'plugins/larablog/membership/database/migrations']);

        app()->register(PaymentProvider::class);
        app()->register(PaidContentProvider::class);
        app()->register(MembershipProvider::class);

        (new MembershipPlanSeeder)->run();
    }

    public function test_membership_requires_payment_plugin(): void
    {
        $manager = app(PluginManager::class);
        $manager->syncDiscoveredPlugins();

        Plugin::query()->where('name', 'larablog/payment')->update(['enabled' => false]);
        Plugin::query()->where('name', 'larablog/membership')->update(['enabled' => false]);

        $this->expectException(RuntimeException::class);
        $manager->enable('larablog/membership');
    }

    public function test_subscribe_monthly_sets_expires_at_and_unlocks_article(): void
    {
        $manager = app(PluginManager::class);
        $manager->syncDiscoveredPlugins();
        $manager->enable('larablog/payment');
        $manager->enable('larablog/membership');

        $user = User::factory()->create();
        [$article] = $this->makeArticle([
            'content' => str_repeat('secret ', 40).'MEMBER_FULL_TOKEN',
        ]);

        ArticleMembership::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'required_plan_id' => null,
        ]);

        $this->get('/show-'.$article->id.'.shtml')
            ->assertOk()
            ->assertDontSee('MEMBER_FULL_TOKEN');

        $plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
        $orders = app(OrderService::class);
        $order = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
        $orders->markPaid($order);

        $status = app(MembershipService::class)->statusFor($user);
        $this->assertTrue($status['active']);
        $this->assertSame('monthly', $status['plan_slug']);
        $this->assertNotNull($status['expires_at']);

        $this->actingAs($user)
            ->get('/show-'.$article->id.'.shtml')
            ->assertOk()
            ->assertSee('MEMBER_FULL_TOKEN');

        $this->actingAs($user)
            ->getJson('/plugins/membership/status')
            ->assertOk()
            ->assertJsonPath('active', true)
            ->assertJsonPath('plan_slug', 'monthly');
    }

    public function test_expired_membership_can_renew(): void
    {
        $user = User::factory()->create();
        $plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
        $orders = app(OrderService::class);

        $first = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
        $orders->markPaid($first);

        Entitlement::query()
            ->where('user_id', $user->id)
            ->where('product_type', ProductType::MEMBERSHIP)
            ->where('product_id', $plan->id)
            ->update(['expires_at' => now()->subDay()]);

        $this->assertFalse($orders->hasEntitlement((int) $user->id, ProductType::MEMBERSHIP, (int) $plan->id));

        $second = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
        $orders->markPaid($second);

        $this->assertTrue($orders->hasEntitlement((int) $user->id, ProductType::MEMBERSHIP, (int) $plan->id));
        $entitlement = Entitlement::query()
            ->where('user_id', $user->id)
            ->where('product_type', ProductType::MEMBERSHIP)
            ->where('product_id', $plan->id)
            ->firstOrFail();
        $this->assertTrue($entitlement->expires_at?->isFuture());
    }

    public function test_required_plan_gate_rejects_other_plan(): void
    {
        $manager = app(PluginManager::class);
        $manager->syncDiscoveredPlugins();
        $manager->enable('larablog/payment');
        $manager->enable('larablog/membership');

        $user = User::factory()->create();
        [$article] = $this->makeArticle(['content' => 'NEED_LIFETIME_TOKEN '.str_repeat('x ', 30)]);
        $monthly = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
        $lifetime = MembershipPlan::query()->where('slug', 'lifetime')->firstOrFail();

        ArticleMembership::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'required_plan_id' => $lifetime->id,
        ]);

        $orders = app(OrderService::class);
        $order = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $monthly->id, $monthly->name, (string) $monthly->price);
        $orders->markPaid($order);

        $decision = app(ArticleAccess::class)->resolve($article, $user);
        $this->assertSame(AccessDecision::NEED_PURCHASE, $decision->status);

        $lifeOrder = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $lifetime->id, $lifetime->name, (string) $lifetime->price);
        $orders->markPaid($lifeOrder);

        $this->assertTrue(app(ArticleAccess::class)->resolve($article, $user)->isAllow());
    }

    public function test_forge_membership_amount_query_is_rejected(): void
    {
        $user = User::factory()->create();
        $plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();

        $this->actingAs($user)
            ->get('/plugins/payment/checkout?'.http_build_query([
                'product_type' => ProductType::MEMBERSHIP,
                'product_id' => $plan->id,
                'amount' => '0.01',
                'title' => 'hack',
            ]))
            ->assertRedirect();

        // Server-side price must be used; after redirect order amount is plan price.
        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
            'amount' => '9.90',
        ]);

        $this->actingAs($user)
            ->get('/plugins/payment/checkout?'.http_build_query([
                'product_type' => 'theme',
                'product_id' => 1,
                'amount' => '0.01',
                'title' => 'hack',
            ]))
            ->assertStatus(422);
    }

    public function test_mutex_validate_hook_rejects_paid_and_membership(): void
    {
        $data = [
            'read_password' => null,
            'paid_content' => ['enabled' => true, 'price' => 1],
            'membership' => ['enabled' => true],
        ];

        $this->expectException(ValidationException::class);
        Hook::filter('filament.article.validate_access_restrictions', $data, null);
    }

    public function test_cannot_delete_plan_with_entitlement(): void
    {
        $user = User::factory()->create();
        $plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
        $orders = app(OrderService::class);
        $order = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
        $orders->markPaid($order);

        $this->assertTrue($plan->fresh()->hasEntitlements());
    }

    /**
     * @param  array<string, mixed>  $overrides
     * @return array{0: Article, 1: User, 2: Category}
     */
    protected function makeArticle(array $overrides = []): array
    {
        $user = isset($overrides['user_id'])
            ? User::query()->findOrFail($overrides['user_id'])
            : User::factory()->create();
        $category = Category::query()->create(['name' => 'Mem', 'display_order' => 0]);

        $article = Article::query()->create(array_merge([
            'category_id' => $category->id,
            'user_id' => $user->id,
            'title' => 'Members post',
            'content' => 'Hello',
            'content_format' => 'markdown',
            'published_at' => now()->subMinute(),
            'visible' => true,
        ], $overrides));

        return [$article, $user, $category];
    }
}
