<?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 Plugins\Larablog\PaidContent\Models\ArticleProduct;
use Plugins\Larablog\PaidContent\PluginServiceProvider as PaidContentProvider;
use Plugins\Larablog\Payment\Domain\OrderService;
use Plugins\Larablog\Payment\Domain\ProductType;
use Plugins\Larablog\Payment\Models\Order;
use Plugins\Larablog\Payment\Models\OrderItem;
use Plugins\Larablog\Payment\PluginServiceProvider as PaymentProvider;
use RuntimeException;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class PaidContentCommerceTest 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']);

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

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

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

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

    public function test_password_article_blocked_on_api_and_rss(): void
    {
        [$article] = $this->makeArticle([
            'content' => str_repeat('SECRET-BODY ', 20),
            'read_password' => 's3cret',
            'description' => 'short desc',
        ]);

        $this->getJson('/api/v1/articles/'.$article->id)
            ->assertOk()
            ->assertJsonPath('data.access.status', AccessDecision::NEED_PASSWORD)
            ->assertJsonPath('data.content', null);

        $rss = (string) $this->get('/rss.xml')->assertOk()->getContent();
        $this->assertStringNotContainsString('SECRET-BODY', $rss);
        $this->assertStringContainsString('short desc', $rss);
    }

    public function test_paid_article_teaser_then_purchase_unlocks_web(): void
    {
        $buyer = User::factory()->create();
        $author = User::factory()->create();
        [$article] = $this->makeArticle([
            'user_id' => $author->id,
            'content' => "## Intro\n\n".str_repeat('teaser ', 10).'UNIQUE_FULL_TOKEN '.str_repeat('tail ', 10),
        ]);

        ArticleProduct::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'price' => '9.90',
            'currency' => 'CNY',
            'trial_mode' => 'chars',
            'trial_value' => 40,
        ]);

        $this->get('/show-'.$article->id.'.shtml')
            ->assertOk()
            ->assertSee(__('frontend.article.paywall_title'))
            ->assertDontSee('UNIQUE_FULL_TOKEN');

        $this->getJson('/api/v1/articles/'.$article->id)
            ->assertOk()
            ->assertJsonPath('data.access.status', AccessDecision::NEED_PURCHASE)
            ->assertJsonPath('data.content', null);

        $orders = app(OrderService::class);
        $order = $orders->createOrder(
            $buyer,
            ProductType::ARTICLE,
            (int) $article->id,
            (string) $article->title,
            '9.90',
        );
        $orders->markPaid($order, ['source' => 'test']);

        $this->actingAs($buyer)
            ->get('/show-'.$article->id.'.shtml')
            ->assertOk()
            ->assertSee('UNIQUE_FULL_TOKEN')
            ->assertDontSee(__('frontend.article.paywall_title'));
    }

    public function test_stub_rejects_duplicate_entitlement_order(): void
    {
        $user = User::factory()->create();
        [$article] = $this->makeArticle();

        $orders = app(OrderService::class);
        $first = $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'T', '1.00');
        $orders->markPaid($first);

        $this->expectException(RuntimeException::class);
        $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'T', '1.00');
    }

    public function test_guest_checkout_redirects_to_login_not_500(): void
    {
        [$article] = $this->makeArticle();
        ArticleProduct::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'price' => '1.00',
            'currency' => 'CNY',
            'trial_mode' => 'chars',
            'trial_value' => 20,
        ]);

        $this->get('/plugins/payment/checkout?'.http_build_query([
            'product_type' => ProductType::ARTICLE,
            'product_id' => $article->id,
        ]))->assertRedirect('/login.shtml');
    }

    public function test_home_list_does_not_leak_paid_body(): void
    {
        [$article] = $this->makeArticle([
            'description' => null,
            'content' => str_repeat('preview ', 30)."\n\nHOME_LEAK_TOKEN\n\n".str_repeat('tail ', 30),
        ]);
        ArticleProduct::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'price' => '2.00',
            'currency' => 'CNY',
            'trial_mode' => 'chars',
            'trial_value' => 20,
        ]);

        $this->get('/')
            ->assertOk()
            ->assertDontSee('HOME_LEAK_TOKEN');
    }

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

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

    public function test_teaser_never_returns_full_body(): void
    {
        [$article] = $this->makeArticle([
            'description' => null,
            'content' => 'TEASER_HEAD short paid body ending with TEASER_TAIL_TOKEN',
        ]);

        ArticleProduct::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'price' => '1.00',
            'currency' => 'CNY',
            'trial_mode' => 'chars',
            'trial_value' => 9999,
        ]);

        $this->get('/show-'.$article->id.'.shtml')
            ->assertOk()
            ->assertSee(__('frontend.article.paywall_title'))
            ->assertDontSee('TEASER_TAIL_TOKEN');

        $this->getJson('/api/v1/articles/'.$article->id)
            ->assertOk()
            ->assertJsonPath('data.access.status', AccessDecision::NEED_PURCHASE)
            ->assertJsonMissing(['teaser_html' => 'TEASER_TAIL_TOKEN']);
    }

    public function test_access_filter_can_only_tighten(): void
    {
        [$article] = $this->makeArticle();

        Hook::listen('article.access', fn (AccessDecision $decision): AccessDecision => $decision->tightenWith(
            AccessDecision::needPurchase('teaser', '/buy')
        ));
        Hook::listen('article.access', fn (AccessDecision $decision): AccessDecision => AccessDecision::allow());
        Hook::listen('article.access', fn (): string => 'not-a-decision');

        $decision = app(ArticleAccess::class)->resolve($article, null, []);

        $this->assertSame(AccessDecision::NEED_PURCHASE, $decision->status);
    }

    public function test_plugin_docs_cannot_escape_plugin_directory(): void
    {
        $root = storage_path('framework/testing/plugins-'.uniqid());
        $pluginDir = $root.'/acme/evil';
        mkdir($pluginDir, 0777, true);
        file_put_contents($pluginDir.'/plugin.json', json_encode([
            'name' => 'acme/evil',
            'title' => 'Evil',
            'version' => '1.0.0',
            'docs' => '../../../../.env',
        ]));

        config(['larablog.plugin_path' => $root]);
        $manager = app(PluginManager::class);

        try {
            $this->assertNull($manager->docsPath('acme/evil'));
            $this->assertNull($manager->readDocs('acme/evil'));
        } finally {
            // Only remove the temp tree this test created.
            unlink($pluginDir.'/plugin.json');
            rmdir($pluginDir);
            rmdir($root.'/acme');
            rmdir($root);
        }
    }

    public function test_duplicate_pending_order_is_reused(): void
    {
        $user = User::factory()->create();
        [$article] = $this->makeArticle();

        $orders = app(OrderService::class);
        $first = $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'T', '1.00');
        $second = $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'T', '1.00');

        $this->assertSame($first->id, $second->id);
    }

    public function test_reused_pending_order_is_repriced(): void
    {
        $user = User::factory()->create();
        [$article] = $this->makeArticle();

        $orders = app(OrderService::class);
        $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'Old', '1.00');
        $reused = $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'New', '9.90');

        $this->assertSame('9.90', (string) $reused->amount);
        $this->assertSame('9.90', (string) $reused->items->first()->amount);
        $this->assertSame('New', $reused->items->first()->title);
        $this->assertSame(1, Order::query()->count());
    }

    public function test_mark_paid_rejects_product_owned_by_another_order(): void
    {
        $user = User::factory()->create();
        [$article] = $this->makeArticle();

        $orders = app(OrderService::class);
        $paid = $orders->createOrder($user, ProductType::ARTICLE, (int) $article->id, 'T', '1.00');
        $orders->markPaid($paid);

        $duplicate = Order::query()->create([
            'user_id' => $user->id,
            'status' => Order::STATUS_PENDING,
            'amount' => '1.00',
            'currency' => 'CNY',
            'gateway' => 'stub',
        ]);
        OrderItem::query()->create([
            'order_id' => $duplicate->id,
            'product_type' => ProductType::ARTICLE,
            'product_id' => $article->id,
            'title' => 'T',
            'amount' => '1.00',
        ]);

        $this->expectException(RuntimeException::class);
        $orders->markPaid($duplicate);
    }

    public function test_checkout_rejects_unpublished_paid_article(): void
    {
        $user = User::factory()->create();
        [$article] = $this->makeArticle(['visible' => false]);

        ArticleProduct::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'price' => '5.00',
            'currency' => 'CNY',
            'trial_mode' => 'chars',
            'trial_value' => 20,
        ]);

        $this->actingAs($user)
            ->get('/plugins/payment/checkout?'.http_build_query([
                'product_type' => ProductType::ARTICLE,
                'product_id' => $article->id,
            ]))
            ->assertStatus(422);

        $this->assertSame(0, Order::query()->count());
    }

    public function test_author_and_admin_bypass_paywall(): void
    {
        Role::findOrCreate('admin');
        $author = User::factory()->create();
        $admin = User::factory()->create();
        $admin->assignRole('admin');

        [$article] = $this->makeArticle([
            'user_id' => $author->id,
            'content' => str_repeat('full-access-body ', 30),
        ]);

        ArticleProduct::query()->create([
            'article_id' => $article->id,
            'enabled' => true,
            'price' => '3.00',
            'currency' => 'CNY',
            'trial_mode' => 'chars',
            'trial_value' => 10,
        ]);

        $access = app(ArticleAccess::class);
        $this->assertTrue($access->resolve($article, $author)->isAllow());
        $this->assertTrue($access->resolve($article, $admin)->isAllow());
        $this->assertSame(AccessDecision::NEED_PURCHASE, $access->resolve($article, null)->status);
    }

    public function test_article_table_hooks_expose_price_column_and_filter(): void
    {
        $columnNames = collect(Hook::collect('filament.article.table.columns'))
            ->map(fn ($column) => $column->getName())
            ->all();
        $this->assertContains('paid_content_enabled', $columnNames);
        $this->assertContains('paid_content_price', $columnNames);

        $filterNames = collect(Hook::collect('filament.article.table.filters'))
            ->map(fn ($filter) => $filter->getName())
            ->all();
        $this->assertContains('paid_enabled', $filterNames);
    }

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

        $payload = array_merge([
            'category_id' => $category->id,
            'user_id' => $user->id,
            'title' => 'Paid Post',
            'content' => 'Hello body',
            'content_format' => 'markdown',
            'published_at' => now()->subMinute(),
            'visible' => true,
        ], $overrides);

        $article = Article::query()->create($payload);

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