<?php

namespace Tests\Tenant;

use App\Services\PageBulkUpdateService;
use Mtc\ContentManager\Models\ContentCategory;
use Mtc\ContentManager\Models\ContentTag;
use Mtc\MercuryDataModels\Franchise;
use Mtc\MercuryDataModels\Page;
use Mtc\MercuryDataModels\SeoData;
use Mtc\MercuryDataModels\User;
use Tests\TenantTestCase;

class PageBulkUpdateServiceTest extends TenantTestCase
{
    // Field metadata enumerates User, Franchise, Template, ContentCategory and
    // ContentTag for relationship options. User lives on the central database,
    // so getFields() (called by every update()) needs master + tenant.
    protected bool $needsMasterDatabase = true;

    public function testSameModeUpdatesPlainColumn(): void
    {
        $pages = Page::factory(3)->create(['title' => 'old']);

        $count = (new PageBulkUpdateService())
            ->update('same', $pages->pluck('id')->all(), ['field' => 'title', 'value' => 'new']);

        $this->assertSame(3, $count);
        foreach ($pages as $page) {
            $this->assertSame('new', $page->fresh()->title);
        }
    }

    public function testSameModeUpdatesSeoSubFieldsWithoutClobberingOthers(): void
    {
        $page = Page::factory()->create([
            'seo' => ['title' => 'Existing title', 'description' => 'Existing description'],
        ]);

        $service = new PageBulkUpdateService();
        $service->update('same', [$page->id], ['field' => 'seo.description', 'value' => 'New description']);
        $service->update('same', [$page->id], ['field' => 'seo.canonical', 'value' => 'https://example.test/canonical']);

        $fresh = $page->fresh();
        $this->assertSame('Existing title', $fresh->seo['title']);
        $this->assertSame('New description', $fresh->seo['description']);
        $this->assertSame('https://example.test/canonical', $fresh->seo['canonical']);
    }

    public function testSameModeMirrorsSeoChangesToSeoDataTable(): void
    {
        $page = Page::factory()->create(['slug' => 'about', 'seo' => ['title' => 'Existing title']]);

        (new PageBulkUpdateService())
            ->update('same', [$page->id], ['field' => 'seo.description', 'value' => 'Live description']);

        $seoData = SeoData::query()->where('path', $page->urlPath())->first();
        $this->assertNotNull($seoData, 'expected seo_data row mirrored from bulk SEO write');
        $this->assertSame('Live description', $seoData->description);
        $this->assertSame('Existing title', $seoData->title);
    }

    public function testTouchingOneSeoFieldDoesNotClobberOtherSeoDataColumns(): void
    {
        // seo_data has a full set of values; pages.seo is missing the
        // description (mimics historical drift between the two stores).
        $page = Page::factory()->create(['slug' => 'about', 'seo' => ['title' => 'Mirror title']]);
        SeoData::query()->create([
            'path' => $page->urlPath(),
            'title' => 'Live title',
            'description' => 'Live description that must survive',
            'canonical' => 'https://example.test/live',
        ]);

        (new PageBulkUpdateService())
            ->update('same', [$page->id], ['field' => 'seo.title', 'value' => 'Updated title']);

        $seoData = SeoData::query()->where('path', $page->urlPath())->first();
        $this->assertSame('Updated title', $seoData->title);
        $this->assertSame('Live description that must survive', $seoData->description);
        $this->assertSame('https://example.test/live', $seoData->canonical);

        // pages.seo mirror should also pick up the seo_data values it
        // previously didn't have, so the admin page-edit form doesn't
        // show an empty description after a partial bulk SEO update.
        $fresh = $page->fresh();
        $this->assertSame('Updated title', $fresh->seo['title']);
        $this->assertSame('Live description that must survive', $fresh->seo['description']);
        $this->assertSame('https://example.test/live', $fresh->seo['canonical']);
    }

    public function testDifferentModeMergesMultipleSeoFieldsAgainstLiveSeoData(): void
    {
        $page = Page::factory()->create(['slug' => 'about']);
        SeoData::query()->create([
            'path' => $page->urlPath(),
            'description' => 'Existing live description',
            'heading' => 'Existing heading',
        ]);

        (new PageBulkUpdateService())->update('different', [$page->id], [
            'updates' => [
                (string) $page->id => [
                    'seo.title' => 'Bulk title',
                    'seo.canonical' => 'https://example.test/new',
                ],
            ],
        ]);

        $seoData = SeoData::query()->where('path', $page->urlPath())->first();
        $this->assertSame('Bulk title', $seoData->title);
        $this->assertSame('https://example.test/new', $seoData->canonical);
        $this->assertSame('Existing live description', $seoData->description);
        $this->assertSame('Existing heading', $seoData->heading);
    }

    public function testGetPageDataPrefersSeoDataOverPagesSeoMirror(): void
    {
        $page = Page::factory()->create([
            'slug' => 'about',
            'seo' => ['description' => 'mirror desc'],
        ]);
        SeoData::query()->create([
            'path' => $page->urlPath(),
            'description' => 'live desc',
        ]);

        $data = (new PageBulkUpdateService())->getPageData([$page->id])[$page->id];

        $this->assertSame('live desc', $data['seo.description']);
    }

    public function testSameModeCastsRelationshipFieldsAsIntegers(): void
    {
        $page = Page::factory()->create();
        $author = User::factory()->create();

        (new PageBulkUpdateService())
            ->update('same', [$page->id], ['field' => 'author_id', 'value' => (string) $author->id]);

        $fresh = $page->fresh();
        $this->assertSame($author->id, $fresh->author_id);
        $this->assertIsInt($fresh->author_id);
    }

    public function testSameModeSyncsCategoriesAsMultiSelect(): void
    {
        $pages = Page::factory(2)->create();
        $catA = ContentCategory::query()->create(['name' => 'Cat A']);
        $catB = ContentCategory::query()->create(['name' => 'Cat B']);

        $count = (new PageBulkUpdateService())
            ->update('same', $pages->pluck('id')->all(), [
                'field' => 'categories',
                'value' => [$catA->id, $catB->id],
            ]);

        $this->assertSame(2, $count);
        foreach ($pages as $page) {
            $ids = $page->fresh()->categories->pluck('id')->sort()->values()->all();
            $this->assertSame([$catA->id, $catB->id], $ids);
        }
    }

    public function testSameModeSyncsTagsAsMultiSelect(): void
    {
        $page = Page::factory()->create();
        $tagA = ContentTag::query()->create(['name' => 'Tag A']);
        $tagB = ContentTag::query()->create(['name' => 'Tag B']);
        $page->tags()->sync([$tagA->id]);

        (new PageBulkUpdateService())
            ->update('same', [$page->id], ['field' => 'tags', 'value' => [$tagB->id]]);

        $this->assertSame([$tagB->id], $page->fresh()->tags->pluck('id')->all());
    }

    public function testDifferentModeMixesPlainAndSeoFields(): void
    {
        $page = Page::factory()->create([
            'title' => 'Original',
            'seo' => ['description' => 'Old'],
        ]);

        $count = (new PageBulkUpdateService())->update(
            'different',
            [$page->id],
            ['updates' => [
                (string) $page->id => [
                    'title' => 'Updated title',
                    'seo.description' => 'Updated description',
                ],
            ]]
        );

        $this->assertSame(1, $count);
        $fresh = $page->fresh();
        $this->assertSame('Updated title', $fresh->title);
        $this->assertSame('Updated description', $fresh->seo['description']);
    }

    public function testGetFieldsExposesRichTypeMetadata(): void
    {
        $fields = collect((new PageBulkUpdateService())->getFields())->keyBy('name');

        $this->assertSame('datetime', $fields['published_at']['type']);
        $this->assertSame('datetime', $fields['unpublished_at']['type']);
        $this->assertSame('password', $fields['password']['type']);
        $this->assertSame('textarea', $fields['excerpt']['type']);
        $this->assertSame('multiselect', $fields['categories']['type']);
        $this->assertSame('multiselect', $fields['tags']['type']);
        $this->assertSame('select', $fields['franchise_id']['type']);
        $this->assertSame('textarea', $fields['seo.description']['type']);
        $this->assertSame('textarea', $fields['seo.jsonSchema']['type']);
        $this->assertNotEmpty($fields['seo.description']['helper_text']);
        $this->assertNotEmpty($fields['published_at']['helper_text']);
    }

    public function testSameModeUpdatesSeoJsonSchemaThroughCastAlias(): void
    {
        $page = Page::factory()->create();

        (new PageBulkUpdateService())
            ->update('same', [$page->id], ['field' => 'seo.jsonSchema', 'value' => '{"@type":"Page"}']);

        // Cast.get() exposes the value under the snake key.
        $this->assertSame('{"@type":"Page"}', $page->fresh()->seo['json_schema']);

        // And it round-trips back through getPageData under the camel key.
        $data = (new PageBulkUpdateService())->getPageData([$page->id])[$page->id];
        $this->assertSame('{"@type":"Page"}', $data['seo.jsonSchema']);
    }

    public function testGetPageDataFlattensSeoAndIncludesMultiRelationIds(): void
    {
        $page = Page::factory()->create([
            'title' => 'Test',
            'seo' => ['description' => 'Has description'],
        ]);
        $cat = ContentCategory::query()->create(['name' => 'Cat']);
        $tag = ContentTag::query()->create(['name' => 'Tag']);
        $page->categories()->sync([$cat->id]);
        $page->tags()->sync([$tag->id]);

        $data = (new PageBulkUpdateService())->getPageData([$page->id])[$page->id];

        $this->assertSame('Has description', $data['seo.description']);
        $this->assertSame([$cat->id], $data['categories']);
        $this->assertSame([$tag->id], $data['tags']);
    }

    public function testAuthorOptionsAreScopedToCurrentTenantsUsers(): void
    {
        // Foreign user that isn't attached to this tenant — must not leak
        // into the author dropdown. Permission filtering itself is tested
        // upstream in PageViewResource; here we only assert the scope.
        User::factory()->create();

        $fields = collect((new PageBulkUpdateService())->getFields())->keyBy('name');
        $authorOptionIds = collect($fields['author_id']['options'])->pluck('value')->all();
        $tenantUserIds = tenant()->users()->pluck('users.id')->all();

        $this->assertEqualsCanonicalizing(
            array_values(array_intersect($tenantUserIds, $authorOptionIds)),
            $authorOptionIds,
            'author options must be a subset of tenant->users()',
        );
        $this->assertGreaterThan(
            $authorOptionIds === [] ? -1 : count($authorOptionIds),
            User::query()->count(),
            'sanity check: there are users in the global table beyond this tenant',
        );
    }

    public function testSameModeUpdatesFranchiseSelect(): void
    {
        $page = Page::factory()->create();
        $franchise = Franchise::factory()->create();

        (new PageBulkUpdateService())
            ->update('same', [$page->id], ['field' => 'franchise_id', 'value' => $franchise->id]);

        $this->assertSame($franchise->id, $page->fresh()->franchise_id);
    }

    public function testArchiveSoftDeletesSelectedPagesAndSkipsTrashed(): void
    {
        $active = Page::factory(2)->create();
        $alreadyArchived = Page::factory()->create();
        $alreadyArchived->delete();

        $ids = $active->pluck('id')->push($alreadyArchived->id)->all();
        $archived = (new PageBulkUpdateService())->archive($ids);

        $this->assertSame(2, $archived, 'Already-archived pages must not be re-counted.');
        foreach ($active as $page) {
            $this->assertTrue($page->fresh()->trashed());
        }
        $this->assertSame(0, Page::query()->whereIn('id', $ids)->count());
        $this->assertSame(3, Page::withTrashed()->whereIn('id', $ids)->count());
    }
}
