<?php

namespace Tests\Tenant;

use App\Facades\Settings;
use App\Imports\Blog\BlogArticleData;
use App\Imports\Blog\CsvBlogParser;
use App\Jobs\BaseArticleImport;
use App\Jobs\BlogImportJob;
use App\Jobs\WordPressArticleImport;
use App\Jobs\WordPressBlogImport;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Storage;
use Mtc\ContentManager\Contracts\PageModel;
use Mtc\ContentManager\Facades\Media as MediaFacade;
use Mtc\ContentManager\Models\ContentCategory;
use Mtc\ContentManager\Models\ContentElementField;
use Mtc\ContentManager\Models\Media;
use Mtc\ContentManager\Models\TemplateElement;
use Mtc\MercuryDataModels\ContentElement;
use Mtc\MercuryDataModels\Template;
use Tests\TenantTestCase;
use Tests\UserForTenant;

class BlogImportTest extends TenantTestCase
{
    use UserForTenant;

    private string $xmlPath;

    protected function setUp(): void
    {
        parent::setUp();
        $this->xmlPath = dirname(__DIR__) . '/data/wordpress-blog-export.xml';
    }

    // -------------------------------------------------------
    // Controller tests
    // -------------------------------------------------------

    public function testImportDispatchesBlogImportJob()
    {
        Bus::fake();
        Storage::fake('file-storage');
        $template = Template::factory()->create(['slug' => 'blog-article']);

        $response = $this->actingAs($this->getUser())
            ->withHeader('X-Tenant', tenant('id'))
            ->postJson(route('tenant.blog.import'), [
                'file' => UploadedFile::fake()->createWithContent(
                    'blog-export.xml',
                    file_get_contents($this->xmlPath)
                ),
                'format' => 'wordpress',
                'template_id' => $template->id,
            ]);

        $response->assertStatus(200);
        $response->assertJson([
            'success' => true,
            'message' => 'Blog import has been queued for processing.',
        ]);

        Bus::assertDispatched(BlogImportJob::class, 1);
    }

    public function testImportStoresFileAndCreatesBulkImportLog()
    {
        Bus::fake();
        Storage::fake('file-storage');
        $template = Template::factory()->create(['slug' => 'blog-article']);

        $this->actingAs($this->getUser())
            ->withHeader('X-Tenant', tenant('id'))
            ->postJson(route('tenant.blog.import'), [
                'file' => UploadedFile::fake()->createWithContent(
                    'blog-export.xml',
                    file_get_contents($this->xmlPath)
                ),
                'format' => 'wordpress',
                'template_id' => $template->id,
            ]);

        // Verify file was stored
        $files = Storage::disk('file-storage')->allFiles('blog-import');
        $this->assertCount(1, $files);
        $this->assertStringStartsWith('blog-import/blog-import-', $files[0]);
        $this->assertStringEndsWith('.xml', $files[0]);

        // Verify BulkImportLog was created
        $this->assertDatabaseHas('bulk_import_log', [
            'object_type' => 'blog',
        ]);
    }

    public function testImportFailsWhenTemplateIdDoesNotExist()
    {
        Bus::fake();
        Storage::fake('file-storage');

        $response = $this->actingAs($this->getUser())
            ->withHeader('X-Tenant', tenant('id'))
            ->postJson(route('tenant.blog.import'), [
                'file' => UploadedFile::fake()->createWithContent(
                    'blog-export.xml',
                    file_get_contents($this->xmlPath)
                ),
                'format' => 'wordpress',
                'template_id' => 999999,
            ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('template_id');

        Bus::assertNotDispatched(BlogImportJob::class);
    }

    public function testImportFailsWithInvalidFormat()
    {
        Bus::fake();
        Storage::fake('file-storage');
        $template = Template::factory()->create(['slug' => 'blog-article']);

        $response = $this->actingAs($this->getUser())
            ->withHeader('X-Tenant', tenant('id'))
            ->postJson(route('tenant.blog.import'), [
                'file' => UploadedFile::fake()->createWithContent(
                    'blog-export.xml',
                    file_get_contents($this->xmlPath)
                ),
                'format' => 'unsupported',
                'template_id' => $template->id,
            ]);

        $response->assertStatus(422);
    }

    public function testImportValidationRequiresFile()
    {
        $response = $this->actingAs($this->getUser())
            ->withHeader('X-Tenant', tenant('id'))
            ->postJson(route('tenant.blog.import'), []);

        $response->assertStatus(422);
    }

    // -------------------------------------------------------
    // WordPressBlogImport job tests
    // -------------------------------------------------------

    public function testJobDispatchesArticleImportsForPublishedPosts()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        Bus::assertDispatched(WordPressArticleImport::class, 3);
    }

    public function testJobSkipsDraftsAndNonPostItems()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        // XML contains: 3 published posts, 1 draft, 1 page, 2 attachments
        // Only the 3 published posts should be dispatched
        Bus::assertDispatched(WordPressArticleImport::class, 3);

        // Verify the dispatched jobs have correct post data
        $dispatchedSlugs = [];
        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) use (&$dispatchedSlugs) {
            $reflection = new \ReflectionClass($job);
            $property = $reflection->getProperty('postData');
            $property->setAccessible(true);
            $postData = $property->getValue($job);
            $dispatchedSlugs[] = $postData['slug'];
            return true;
        });

        $this->assertContains('first-blog-post', $dispatchedSlugs);
        $this->assertContains('second-blog-post', $dispatchedSlugs);
        $this->assertContains('third-blog-post', $dispatchedSlugs);
        $this->assertNotContains('draft-post', $dispatchedSlugs);
        $this->assertNotContains('about-us', $dispatchedSlugs);
    }

    public function testJobResolvesFeaturedImageFromAttachments()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        // Check that the first post job has the correct featured image URL
        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);

            $postDataProp = $reflection->getProperty('postData');
            $postDataProp->setAccessible(true);
            $postData = $postDataProp->getValue($job);

            $imageProp = $reflection->getProperty('featuredImageUrl');
            $imageProp->setAccessible(true);
            $featuredImageUrl = $imageProp->getValue($job);

            if ($postData['slug'] === 'first-blog-post') {
                return $featuredImageUrl === 'https://example.com/wp-content/uploads/2024/01/featured-one.jpg';
            }

            if ($postData['slug'] === 'second-blog-post') {
                return $featuredImageUrl === 'https://example.com/wp-content/uploads/2024/02/featured-two.jpg';
            }

            if ($postData['slug'] === 'third-blog-post') {
                return $featuredImageUrl === null;
            }

            return true;
        });
    }

    public function testJobExtractsCorrectPostData()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);

            $postDataProp = $reflection->getProperty('postData');
            $postDataProp->setAccessible(true);
            $postData = $postDataProp->getValue($job);

            if ($postData['slug'] !== 'first-blog-post') {
                return true;
            }

            return $postData['title'] === 'First Blog Post'
                && $postData['date'] === '2024-01-15 10:30:00'
                && str_contains($postData['content'], 'first blog post content')
                && $postData['excerpt'] === 'This is the excerpt for the first post.';
        });
    }

    public function testJobHandlesEmptyXmlWithNoPublishedPosts()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $emptyXml = '<?xml version="1.0" encoding="UTF-8" ?>
        <rss version="2.0"
            xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
            xmlns:content="http://purl.org/rss/1.0/modules/content/"
            xmlns:dc="http://purl.org/dc/elements/1.1/"
            xmlns:wp="http://wordpress.org/export/1.2/"
        >
        <channel>
            <title>Empty Blog</title>
            <item>
                <title>Draft Only</title>
                <wp:post_id>1</wp:post_id>
                <wp:post_name>draft-only</wp:post_name>
                <wp:post_date>2024-01-01 00:00:00</wp:post_date>
                <wp:post_type>post</wp:post_type>
                <wp:status>draft</wp:status>
                <content:encoded><![CDATA[<p>Draft content</p>]]></content:encoded>
                <excerpt:encoded><![CDATA[]]></excerpt:encoded>
            </item>
        </channel>
        </rss>';

        $filePath = 'blog-import/test-empty.xml';
        Storage::disk('file-storage')->put($filePath, $emptyXml);

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        Bus::assertNotDispatched(WordPressArticleImport::class);
    }

    public function testJobExtractsCategoriesFromXml()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        $dispatched = [];
        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) use (&$dispatched) {
            $reflection = new \ReflectionClass($job);

            $postDataProp = $reflection->getProperty('postData');
            $postDataProp->setAccessible(true);
            $slug = $postDataProp->getValue($job)['slug'];

            $categoriesProp = $reflection->getProperty('categories');
            $categoriesProp->setAccessible(true);
            $dispatched[$slug] = $categoriesProp->getValue($job);

            return true;
        });

        // First post has two categories (post_tag "Featured" should be excluded)
        $this->assertCount(2, $dispatched['first-blog-post']);
        $this->assertEquals('news', $dispatched['first-blog-post'][0]['slug']);
        $this->assertEquals('News', $dispatched['first-blog-post'][0]['name']);
        $this->assertEquals('motorhome-holidays', $dispatched['first-blog-post'][1]['slug']);
        $this->assertEquals('Motorhome Holidays', $dispatched['first-blog-post'][1]['name']);

        // Second post has one category
        $this->assertCount(1, $dispatched['second-blog-post']);
        $this->assertEquals('road-trips', $dispatched['second-blog-post'][0]['slug']);
        $this->assertEquals('Road Trips', $dispatched['second-blog-post'][0]['name']);

        // Third post has no categories
        $this->assertEmpty($dispatched['third-blog-post']);
    }

    public function testArticleImportCreatesCategoriesAndSyncsToPage()
    {
        Template::factory()->create(['slug' => 'blog-article']);

        $postData = [
            'title' => 'Test Post With Categories',
            'slug' => 'test-post-with-categories',
            'date' => '2024-01-15 10:30:00',
            'content' => '<p>Test content.</p>',
            'excerpt' => 'Test excerpt.',
            'seo_title' => null,
            'seo_description' => null,
        ];

        $categories = [
            ['slug' => 'motorhome-holidays', 'name' => 'Motorhome Holidays'],
            ['slug' => 'road-trips', 'name' => 'Road Trips'],
        ];

        $job = new WordPressArticleImport($postData, null, 'blog-article', $categories);
        $job->handle();

        // Assert content_categories were created
        $this->assertDatabaseHas('content_categories', ['slug' => 'motorhome-holidays', 'name' => 'Motorhome Holidays']);
        $this->assertDatabaseHas('content_categories', ['slug' => 'road-trips', 'name' => 'Road Trips']);

        // Assert the page was linked to both categories via the pivot table
        $page = app(PageModel::class)->where('slug', 'test-post-with-categories')->first();
        $this->assertNotNull($page);
        $this->assertCount(2, $page->categories);
        $this->assertEquals(
            ['motorhome-holidays', 'road-trips'],
            $page->categories->pluck('slug')->sort()->values()->toArray()
        );
    }

    public function testArticleImportDoesNotDuplicateExistingCategories()
    {
        Template::factory()->create(['slug' => 'blog-article']);

        // Pre-create a category
        ContentCategory::create(['slug' => 'road-trips', 'name' => 'Road Trips']);

        $postData = [
            'title' => 'Another Post',
            'slug' => 'another-post',
            'date' => '2024-02-01 12:00:00',
            'content' => '<p>Content.</p>',
            'excerpt' => '',
            'seo_title' => null,
            'seo_description' => null,
        ];

        $categories = [
            ['slug' => 'road-trips', 'name' => 'Road Trips'],
            ['slug' => 'news', 'name' => 'News'],
        ];

        $job = new WordPressArticleImport($postData, null, 'blog-article', $categories);
        $job->handle();

        // Should still only have one "road-trips" category, not a duplicate
        $this->assertEquals(1, ContentCategory::where('slug', 'road-trips')->count());

        // But "news" should have been created
        $this->assertDatabaseHas('content_categories', ['slug' => 'news', 'name' => 'News']);

        // Page should be linked to both
        $page = app(PageModel::class)->where('slug', 'another-post')->first();
        $this->assertCount(2, $page->categories);
    }

    public function testArticleImportWithNoCategoriesCreatesNoPivotRows()
    {
        Template::factory()->create(['slug' => 'blog-article']);

        $postData = [
            'title' => 'No Categories Post',
            'slug' => 'no-categories-post',
            'date' => '2024-03-01 09:00:00',
            'content' => '<p>Content.</p>',
            'excerpt' => '',
            'seo_title' => null,
            'seo_description' => null,
        ];

        $job = new WordPressArticleImport($postData, null, 'blog-article', []);
        $job->handle();

        $page = app(PageModel::class)->where('slug', 'no-categories-post')->first();
        $this->assertNotNull($page);
        $this->assertCount(0, $page->categories);
    }

    public function testJobUsesTemplateFromSettingWhenSet()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');

        $template = Template::factory()->create(['slug' => 'custom-blog-template']);

        Settings::shouldReceive('get')
            ->with('app-content-blog-default-template')
            ->andReturn($template->id);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);
            $property = $reflection->getProperty('templateSlug');
            $property->setAccessible(true);
            return $property->getValue($job) === 'custom-blog-template';
        });
    }

    public function testJobIgnoresYoastTitleContainingPercentTokens(): void
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);
            $prop = $reflection->getProperty('postData');
            $prop->setAccessible(true);
            $postData = $prop->getValue($job);

            if ($postData['slug'] !== 'first-blog-post') {
                return true;
            }

            // Yoast title "%%title%% | %%sitename%%" should be discarded → null
            return $postData['seo_title'] === null;
        });
    }

    public function testJobUsesLiteralYoastTitle(): void
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);
            $prop = $reflection->getProperty('postData');
            $prop->setAccessible(true);
            $postData = $prop->getValue($job);

            if ($postData['slug'] !== 'second-blog-post') {
                return true;
            }

            return $postData['seo_title'] === 'Custom Title for Second Post';
        });
    }

    public function testJobDerivesSlugFromPostLink(): void
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $xml = <<<XML
        <?xml version="1.0" encoding="UTF-8" ?>
        <rss version="2.0"
            xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
            xmlns:content="http://purl.org/rss/1.0/modules/content/"
            xmlns:wp="http://wordpress.org/export/1.2/"
        >
        <channel>
            <item>
                <title>My Article</title>
                <link>https://example.com/blog/2024/my-article-slug/</link>
                <content:encoded><![CDATA[<p>Body</p>]]></content:encoded>
                <excerpt:encoded><![CDATA[]]></excerpt:encoded>
                <wp:post_id>1</wp:post_id>
                <wp:post_date>2024-01-01 00:00:00</wp:post_date>
                <wp:post_type>post</wp:post_type>
                <wp:status>publish</wp:status>
            </item>
        </channel>
        </rss>
        XML;

        $filePath = 'blog-import/slug-test.xml';
        Storage::disk('file-storage')->put($filePath, $xml);

        (new WordPressBlogImport($filePath))->handle();

        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);
            $prop = $reflection->getProperty('postData');
            $prop->setAccessible(true);
            return $prop->getValue($job)['slug'] === 'my-article-slug';
        });
    }

    public function testJobFallsBackToTitleSlugWhenLinkIsAbsent(): void
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $xml = <<<XML
        <?xml version="1.0" encoding="UTF-8" ?>
        <rss version="2.0"
            xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
            xmlns:content="http://purl.org/rss/1.0/modules/content/"
            xmlns:wp="http://wordpress.org/export/1.2/"
        >
        <channel>
            <item>
                <title>My Untitled Article</title>
                <content:encoded><![CDATA[<p>Body</p>]]></content:encoded>
                <excerpt:encoded><![CDATA[]]></excerpt:encoded>
                <wp:post_id>1</wp:post_id>
                <wp:post_date>2024-01-01 00:00:00</wp:post_date>
                <wp:post_type>post</wp:post_type>
                <wp:status>publish</wp:status>
            </item>
        </channel>
        </rss>
        XML;

        $filePath = 'blog-import/no-link-test.xml';
        Storage::disk('file-storage')->put($filePath, $xml);

        (new WordPressBlogImport($filePath))->handle();

        Bus::assertDispatched(WordPressArticleImport::class, function (WordPressArticleImport $job) {
            $reflection = new \ReflectionClass($job);
            $prop = $reflection->getProperty('postData');
            $prop->setAccessible(true);
            return $prop->getValue($job)['slug'] === 'my-untitled-article';
        });
    }

    public function testJobDeletesFileAfterProcessing()
    {
        Bus::fake([WordPressArticleImport::class]);
        Storage::fake('file-storage');
        Template::factory()->create(['slug' => 'blog-article']);

        $filePath = 'blog-import/test-import.xml';
        Storage::disk('file-storage')->put($filePath, file_get_contents($this->xmlPath));

        $this->assertTrue(Storage::disk('file-storage')->exists($filePath));

        $job = new WordPressBlogImport($filePath);
        $job->handle();

        $this->assertFalse(Storage::disk('file-storage')->exists($filePath));
    }

    public function testCsvParserPreservesNewlinesInQuotedBodyFields(): void
    {
        Storage::fake('local');

        $multilineBody = "Line one of the body.\n"
            . "Line two follows.\n\n"
            . "Line four after a blank line.";

        $csv = "title,body\n"
            . '"First Article","' . str_replace('"', '""', $multilineBody) . '"' . "\n"
            . '"Second Article","Single line body"' . "\n";

        $parser = new CsvBlogParser(
            columnMapping: ['title' => 'title'],
            contentElementMapping: [1 => ['body' => 'body']],
        );

        $articles = $parser->parse($csv);

        $this->assertCount(2, $articles);
        $this->assertSame('First Article', $articles[0]->title);
        $this->assertSame($multilineBody, $articles[0]->contentElementValues[1]['body']);
        $this->assertSame('Second Article', $articles[1]->title);
        $this->assertSame('Single line body', $articles[1]->contentElementValues[1]['body']);
    }

    public function testCsvParserExtractsSlugFromUrlColumn(): void
    {
        Storage::fake('local');

        $csv = "title,url\n"
            . '"Full URL Article","https://mydomain.com/blog/full-url-article"' . "\n"
            . '"Schemeless URL Article","mydomain.com/blog/schemeless-article"' . "\n"
            . '"Trailing Slash Article","https://mydomain.com/blog/trailing-slash-article/"' . "\n"
            . '"Query String Article","https://mydomain.com/blog/query-article?utm=x"' . "\n";

        $parser = new CsvBlogParser([
            'title' => 'title',
            'url' => 'url',
        ]);

        $articles = $parser->parse($csv);

        $this->assertCount(4, $articles);
        $this->assertSame('full-url-article', $articles[0]->slug);
        $this->assertSame('schemeless-article', $articles[1]->slug);
        $this->assertSame('trailing-slash-article', $articles[2]->slug);
        $this->assertSame('query-article', $articles[3]->slug);
    }

    public function testCsvParserPrefersExplicitSlugOverUrlColumn(): void
    {
        Storage::fake('local');

        $csv = "title,slug,url\n"
            . '"My Article","chosen-slug","https://mydomain.com/blog/ignored-slug"' . "\n";

        $parser = new CsvBlogParser([
            'title' => 'title',
            'slug' => 'slug',
            'url' => 'url',
        ]);

        $articles = $parser->parse($csv);

        $this->assertCount(1, $articles);
        $this->assertSame('chosen-slug', $articles[0]->slug);
    }

    public function testCsvParserFallsBackToTitleSlugWhenNoSlugOrUrlMapped(): void
    {
        Storage::fake('local');

        $csv = "title\n"
            . '"My Fallback Article"' . "\n";

        $parser = new CsvBlogParser([
            'title' => 'title',
        ]);

        $articles = $parser->parse($csv);

        $this->assertCount(1, $articles);
        $this->assertSame('my-fallback-article', $articles[0]->slug);
    }

    public function testCsvParserStampsDateFormatOnArticles(): void
    {
        Storage::fake('local');

        $csv = "title,published_at\n"
            . '"My Article","28/04/2023"' . "\n";

        $parser = new CsvBlogParser(
            columnMapping: ['title' => 'title', 'published_at' => 'published_at'],
            dateFormat: 'd/m/Y',
        );

        $articles = $parser->parse($csv);

        $this->assertCount(1, $articles);
        $this->assertSame('28/04/2023', $articles[0]->publishedAt);
        $this->assertSame('d/m/Y', $articles[0]->dateFormat);
    }

    public function testCsvParserDefaultsDateFormatToUk(): void
    {
        Storage::fake('local');

        $csv = "title\n" . '"My Article"' . "\n";

        $parser = new CsvBlogParser(['title' => 'title']);

        $articles = $parser->parse($csv);

        $this->assertSame('d/m/Y', $articles[0]->dateFormat);
    }

    public function testCsvParserAppliesDefaultPageCategoryWhenColumnNotMapped(): void
    {
        Storage::fake('local');

        $csv = "title\n" . '"My News Article"' . "\n";

        $parser = new CsvBlogParser(
            columnMapping: ['title' => 'title'],
            defaultPageCategory: 'news',
        );

        $articles = $parser->parse($csv);

        $this->assertCount(1, $articles);
        $this->assertSame('news', $articles[0]->pageCategory);
    }

    public function testCsvParserDefaultsPageCategoryToBlog(): void
    {
        Storage::fake('local');

        $csv = "title\n" . '"My Article"' . "\n";

        $parser = new CsvBlogParser(['title' => 'title']);

        $articles = $parser->parse($csv);

        $this->assertSame('blog', $articles[0]->pageCategory);
    }

    public function testCsvParserMappedPageCategoryWinsOverDefault(): void
    {
        Storage::fake('local');

        $csv = "title,page_category\n"
            . '"My Article","reviews"' . "\n";

        $parser = new CsvBlogParser(
            columnMapping: ['title' => 'title', 'page_category' => 'page_category'],
            defaultPageCategory: 'news',
        );

        $articles = $parser->parse($csv);

        $this->assertSame('reviews', $articles[0]->pageCategory);
    }

    public function testBaseArticleImportParsesUkFormattedPublishedDate(): void
    {
        Template::factory()->create(['slug' => 'blog-article']);

        $article = new BlogArticleData(
            title: 'UK Date Article',
            slug: 'uk-date-article',
            publishedAt: '28/04/2023',
            dateFormat: 'd/m/Y',
        );

        (new BaseArticleImport($article))->handle();

        $page = app(PageModel::class)->where('slug', 'uk-date-article')->first();
        $this->assertNotNull($page);
        $this->assertSame('2023-04-28', $page->published_at->format('Y-m-d'));
    }

    public function testBaseArticleImportFallsBackToCarbonParseForIsoDates(): void
    {
        Template::factory()->create(['slug' => 'blog-article']);

        $article = new BlogArticleData(
            title: 'ISO Date Article',
            slug: 'iso-date-article',
            publishedAt: '2023-04-28 10:30:00',
            dateFormat: null,
        );

        (new BaseArticleImport($article))->handle();

        $page = app(PageModel::class)->where('slug', 'iso-date-article')->first();
        $this->assertNotNull($page);
        $this->assertSame('2023-04-28', $page->published_at->format('Y-m-d'));
    }

    public function testBaseArticleImportSavesNullPublishedAtWhenEmpty(): void
    {
        Template::factory()->create(['slug' => 'blog-article']);

        $article = new BlogArticleData(
            title: 'Article With No Date',
            slug: 'article-with-no-date',
            publishedAt: null,
        );

        (new BaseArticleImport($article))->handle();

        $page = app(PageModel::class)->where('slug', 'article-with-no-date')->first();
        $this->assertNotNull($page);
        $this->assertNull($page->published_at);
    }

    public function testBaseArticleImportSavesNullPublishedAtWhenUnparseable(): void
    {
        Template::factory()->create(['slug' => 'blog-article']);

        $article = new BlogArticleData(
            title: 'Article With Bad Date',
            slug: 'article-with-bad-date',
            publishedAt: 'not-a-real-date',
            dateFormat: 'd/m/Y',
        );

        (new BaseArticleImport($article))->handle();

        $page = app(PageModel::class)->where('slug', 'article-with-bad-date')->first();
        $this->assertNotNull($page);
        $this->assertNull($page->published_at);
    }

    public function testBaseArticleImportRewritesInlineImageUrlsInEditorContent(): void
    {
        $element = ContentElement::factory()->create(['slug' => 'body-element']);
        ContentElementField::factory()->create([
            'element_id' => $element->id,
            'slug' => 'body',
            'field_type' => 'editor',
            'name' => 'Body',
            'order' => 0,
        ]);
        $template = Template::factory()->create(['slug' => 'blog-article']);
        $templateElement = TemplateElement::factory()->create([
            'template_id' => $template->id,
            'element_id' => $element->id,
            'order' => 0,
        ]);

        $importedImage = Media::factory()->create([
            'src' => 'imported-photo.jpg',
            'path' => 'blog-images/',
        ]);

        MediaFacade::shouldReceive('importImageFromUrl')
            ->once()
            ->with('https://external.example.com/photo.jpg')
            ->andReturn($importedImage);

        // The fake tenant used by TenantTestCase has no master DB, so the central "domains"
        // table is absent. initializeFakeTenant() already hand-stubs primaryDomain for the
        // same reason; stub the domains relation likewise so getInternalDomains() does not
        // lazy-query a missing table. Empty is correct here: this test is about inline images,
        // not internal-link simplification.
        tenant()->setRelation('domains', collect());

        $externalUrl = 'https://external.example.com/photo.jpg';
        $article = new BlogArticleData(
            title: 'Article With Inline Image',
            slug: 'article-with-inline-image',
            templateSlug: 'blog-article',
            contentElementValues: [
                $templateElement->id => [
                    'body' => '<p>Intro</p><img src="' . $externalUrl . '" alt="x"><p>End</p>',
                ],
            ],
        );

        (new BaseArticleImport($article))->handle();

        $page = app(PageModel::class)->where('slug', 'article-with-inline-image')->first();
        $this->assertNotNull($page);

        $page->loadMissing('content.subContent');
        $body = $page->content
            ->flatMap->subContent
            ->firstWhere('slug', 'body');

        $this->assertNotNull($body, 'Expected a "body" subContent row after sync');
        $this->assertStringNotContainsString(
            $externalUrl,
            $body->content,
            'The original external image URL should be replaced with our own.'
        );
        $this->assertStringContainsString(
            $importedImage->original_url,
            $body->content,
            'The inline image should point to the imported media URL.'
        );
    }

    public function testBaseArticleImportRewritesInlineImageUrlsInTextContent(): void
    {
        $element = ContentElement::factory()->create(['slug' => 'body-text-element']);
        ContentElementField::factory()->create([
            'element_id' => $element->id,
            'slug' => 'body',
            'field_type' => 'text',
            'name' => 'Body',
            'order' => 0,
        ]);
        $template = Template::factory()->create(['slug' => 'blog-article']);
        $templateElement = TemplateElement::factory()->create([
            'template_id' => $template->id,
            'element_id' => $element->id,
            'order' => 0,
        ]);

        $importedImage = Media::factory()->create([
            'src' => 'imported-photo.jpg',
            'path' => 'blog-images/',
        ]);

        MediaFacade::shouldReceive('importImageFromUrl')
            ->once()
            ->with('https://external.example.com/photo.jpg')
            ->andReturn($importedImage);

        // See the editor-content test above for why the domains relation is stubbed empty here.
        tenant()->setRelation('domains', collect());

        $externalUrl = 'https://external.example.com/photo.jpg';
        $article = new BlogArticleData(
            title: 'Article With Inline Image In Text Field',
            slug: 'article-with-inline-image-text',
            templateSlug: 'blog-article',
            contentElementValues: [
                $templateElement->id => [
                    'body' => '<p>Intro</p><img src="' . $externalUrl . '" alt="x"><p>End</p>',
                ],
            ],
        );

        (new BaseArticleImport($article))->handle();

        $page = app(PageModel::class)->where('slug', 'article-with-inline-image-text')->first();
        $this->assertNotNull($page);

        $page->loadMissing('content.subContent');
        $body = $page->content
            ->flatMap->subContent
            ->firstWhere('slug', 'body');

        $this->assertNotNull($body, 'Expected a "body" subContent row after sync');
        $this->assertStringNotContainsString(
            $externalUrl,
            $body->content,
            'External image URLs in a text field should also be replaced with our own.'
        );
        $this->assertStringContainsString(
            $importedImage->original_url,
            $body->content,
            'The inline image should point to the imported media URL.'
        );
    }

    public function testCsvParserReadsAuthorColumn(): void
    {
        Storage::fake('local');

        $csv = "title,author\n"
            . '"First Article","Jane Smith"' . "\n"
            . '"Second Article",""' . "\n";

        $parser = new CsvBlogParser([
            'title' => 'title',
            'author' => 'author',
        ]);

        $articles = $parser->parse($csv);

        $this->assertCount(2, $articles);
        $this->assertSame('Jane Smith', $articles[0]->authorName);
        $this->assertNull($articles[1]->authorName);
    }
}
