<?php
namespace Tests\Unit\Domain\Article;

use PHPUnit\Framework\TestCase;
use Domain\Article\ArticleRepository;

class ArticleRepositoryTest extends TestCase
{
    public function testFindReturnsArticleWithRelations(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_articles', '*', ['id' => 7])
            ->willReturn(['id' => 7, 'status' => 1]);

        $mockDb->expects($this->exactly(4))
            ->method('select')
            ->willReturnOnConsecutiveCalls(
                [
                    ['lang_id' => 'pl', 'title' => 'Artykul'],
                    ['lang_id' => 'en', 'title' => 'Article'],
                ],
                [
                    ['id' => 10, 'src' => '/img/a.jpg']
                ],
                [
                    ['id' => 20, 'src' => '/files/a.pdf']
                ],
                [1, 2]
            );

        $repository = new ArticleRepository($mockDb);
        $article = $repository->find(7);

        $this->assertIsArray($article);
        $this->assertEquals(7, $article['id']);
        $this->assertArrayHasKey('languages', $article);
        $this->assertEquals('Artykul', $article['languages']['pl']['title']);
        $this->assertCount(1, $article['images']);
        $this->assertCount(1, $article['files']);
        $this->assertEquals([1, 2], $article['pages']);
    }

    public function testFindReturnsNullWhenArticleDoesNotExist(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_articles', '*', ['id' => 999])
            ->willReturn(false);
        $mockDb->expects($this->never())->method('select');

        $repository = new ArticleRepository($mockDb);
        $article = $repository->find(999);

        $this->assertNull($article);
    }

    public function testDeleteNonassignedFilesDeletesDbRows(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('select')
            ->with('pp_articles_files', '*', ['article_id' => null])
            ->willReturn([
                ['id' => 1, 'src' => '/this/path/does/not/exist-file.tmp']
            ]);

        $mockDb->expects($this->once())
            ->method('delete')
            ->with('pp_articles_files', ['article_id' => null]);

        $repository = new ArticleRepository($mockDb);
        $repository->deleteNonassignedFiles();

        $this->assertTrue(true);
    }

    public function testDeleteNonassignedImagesDeletesDbRows(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('select')
            ->with('pp_articles_images', '*', ['article_id' => null])
            ->willReturn([
                ['id' => 1, 'src' => '/this/path/does/not/exist-image.tmp']
            ]);

        $mockDb->expects($this->once())
            ->method('delete')
            ->with('pp_articles_images', ['article_id' => null]);

        $repository = new ArticleRepository($mockDb);
        $repository->deleteNonassignedImages();

        $this->assertTrue(true);
    }

    private function getSampleData(): array
    {
        return [
            'title' => ['pl' => 'Testowy artykul', 'en' => 'Test article'],
            'main_image' => ['pl' => '/img/pl.jpg', 'en' => ''],
            'entry' => ['pl' => 'Wstep', 'en' => 'Entry'],
            'text' => ['pl' => 'Tresc', 'en' => 'Content'],
            'table_of_contents' => ['pl' => '', 'en' => ''],
            'status' => 'on',
            'show_title' => 'on',
            'show_table_of_contents' => '',
            'show_date_add' => 'on',
            'date_add' => '',
            'show_date_modify' => '',
            'date_modify' => '',
            'seo_link' => ['pl' => 'testowy-artykul', 'en' => 'test-article'],
            'meta_title' => ['pl' => 'Meta PL', 'en' => ''],
            'meta_description' => ['pl' => '', 'en' => ''],
            'meta_keywords' => ['pl' => '', 'en' => ''],
            'layout_id' => '2',
            'pages' => ['1', '3'],
            'noindex' => ['pl' => '', 'en' => 'on'],
            'repeat_entry' => '',
            'copy_from' => ['pl' => '', 'en' => ''],
            'social_icons' => 'on',
            'block_direct_access' => ['pl' => '', 'en' => ''],
        ];
    }

    public function testSaveCreatesNewArticle(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $data = $this->getSampleData();

        $insertCalls = [];
        $mockDb->method('insert')
            ->willReturnCallback(function ($table, $row) use (&$insertCalls) {
                $insertCalls[] = ['table' => $table, 'row' => $row];
                return true;
            });

        $mockDb->expects($this->once())
            ->method('id')
            ->willReturn(42);

        $mockDb->method('select')->willReturn([]);
        $mockDb->method('max')->willReturn(5);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->save(0, $data, 1);

        $this->assertEquals(42, $result);

        // Verify article insert
        $articleInsert = $insertCalls[0];
        $this->assertEquals('pp_articles', $articleInsert['table']);
        $this->assertEquals(1, $articleInsert['row']['status']);
        $this->assertEquals(1, $articleInsert['row']['show_title']);
        $this->assertEquals(2, $articleInsert['row']['layout_id']);
        $this->assertArrayHasKey('date_add', $articleInsert['row']);
    }

    public function testSaveReturnsZeroWhenInsertFails(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('insert');
        $mockDb->method('id')->willReturn(null);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->save(0, $this->getSampleData(), 1);

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

    public function testSaveUpdatesExistingArticle(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $data = $this->getSampleData();

        $updateCalls = [];
        $mockDb->method('update')
            ->willReturnCallback(function ($table, $row, $where = null) use (&$updateCalls) {
                $updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where];
                return true;
            });

        $mockDb->method('get')->willReturn(99);
        $mockDb->method('select')->willReturn([]);
        $mockDb->method('max')->willReturn(0);
        $mockDb->method('insert')->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->save(10, $data, 1);

        $this->assertEquals(10, $result);

        // Verify article update
        $articleUpdate = $updateCalls[0];
        $this->assertEquals('pp_articles', $articleUpdate['table']);
        $this->assertEquals(1, $articleUpdate['row']['status']);
        $this->assertArrayNotHasKey('date_add', $articleUpdate['row']);
        $this->assertEquals(['id' => 10], $articleUpdate['where']);
    }

    public function testSaveTranslationsInsertsForNewArticle(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $data = $this->getSampleData();

        // 1 insert for pp_articles + 2 inserts for translations (pl, en) + 2 inserts for pages
        $insertCalls = [];
        $mockDb->method('insert')
            ->willReturnCallback(function ($table, $row) use (&$insertCalls) {
                $insertCalls[] = ['table' => $table, 'row' => $row];
                return true;
            });

        $mockDb->method('id')->willReturn(50);
        $mockDb->method('select')->willReturn([]);
        $mockDb->method('max')->willReturn(0);

        $repository = new ArticleRepository($mockDb);
        $repository->save(0, $data, 1);

        $langInserts = array_filter($insertCalls, function ($c) {
            return $c['table'] === 'pp_articles_langs';
        });

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

        $plInsert = array_values(array_filter($langInserts, function ($c) {
            return $c['row']['lang_id'] === 'pl';
        }))[0]['row'];

        $this->assertEquals(50, $plInsert['article_id']);
        $this->assertEquals('Testowy artykul', $plInsert['title']);
        $this->assertEquals('/img/pl.jpg', $plInsert['main_image']);
    }

    public function testSaveTranslationsUpsertsForExistingArticle(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $data = $this->getSampleData();

        // get returns translation ID for 'pl', null for 'en'
        $mockDb->method('get')
            ->willReturnOnConsecutiveCalls(100, null);

        $updateCalls = [];
        $mockDb->method('update')
            ->willReturnCallback(function ($table, $row, $where = null) use (&$updateCalls) {
                $updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where];
                return true;
            });

        $insertCalls = [];
        $mockDb->method('insert')
            ->willReturnCallback(function ($table, $row) use (&$insertCalls) {
                $insertCalls[] = ['table' => $table, 'row' => $row];
                return true;
            });

        $mockDb->method('select')->willReturn([]);
        $mockDb->method('max')->willReturn(0);

        $repository = new ArticleRepository($mockDb);
        $repository->save(10, $data, 1);

        // pl should be updated (translation_id=100)
        $langUpdates = array_filter($updateCalls, function ($c) {
            return $c['table'] === 'pp_articles_langs';
        });
        $this->assertCount(1, $langUpdates);

        // en should be inserted (no existing translation)
        $langInserts = array_filter($insertCalls, function ($c) {
            return $c['table'] === 'pp_articles_langs';
        });
        $this->assertCount(1, $langInserts);
    }

    public function testSavePagesForNewArticle(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $data = $this->getSampleData();
        $data['pages'] = ['5', '8'];

        $insertCalls = [];
        $mockDb->method('insert')
            ->willReturnCallback(function ($table, $row) use (&$insertCalls) {
                $insertCalls[] = ['table' => $table, 'row' => $row];
                return true;
            });

        $mockDb->method('id')->willReturn(60);
        $mockDb->method('select')->willReturn([]);
        $mockDb->method('max')->willReturn(10);

        $repository = new ArticleRepository($mockDb);
        $repository->save(0, $data, 1);

        $pageInserts = array_filter($insertCalls, function ($c) {
            return $c['table'] === 'pp_articles_pages';
        });

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

        $pageIds = array_map(function ($c) {
            return $c['row']['page_id'];
        }, array_values($pageInserts));

        $this->assertContains(5, $pageIds);
        $this->assertContains(8, $pageIds);
    }

    public function testSaveDeletesMarkedImagesOnUpdate(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $data = $this->getSampleData();
        $data['pages'] = null;

        $mockDb->method('update')->willReturn(true);
        $mockDb->method('get')->willReturn(null);
        $mockDb->method('max')->willReturn(0);

        $selectCalls = 0;
        $mockDb->method('select')
            ->willReturnCallback(function ($table, $columns, $where) use (&$selectCalls) {
                $selectCalls++;
                // Return marked images for deletion query
                if ($table === 'pp_articles_images' && isset($where['AND']['to_delete'])) {
                    return [['id' => 1, 'src' => '/nonexistent/path/img.jpg']];
                }
                if ($table === 'pp_articles_files' && isset($where['AND']['to_delete'])) {
                    return [['id' => 2, 'src' => '/nonexistent/path/file.pdf']];
                }
                return [];
            });

        $deleteCalls = [];
        $mockDb->method('delete')
            ->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
                $deleteCalls[] = ['table' => $table, 'where' => $where];
                return true;
            });

        $mockDb->method('insert')->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $repository->save(15, $data, 1);

        $imageDeletes = array_filter($deleteCalls, function ($c) {
            return $c['table'] === 'pp_articles_images';
        });
        $fileDeletes = array_filter($deleteCalls, function ($c) {
            return $c['table'] === 'pp_articles_files';
        });

        $this->assertNotEmpty($imageDeletes);
        $this->assertNotEmpty($fileDeletes);
    }

    public function testSaveGalleryOrderUpdatesImageOrder(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->exactly(3))
            ->method('update')
            ->withConsecutive(
                [
                    'pp_articles_images',
                    ['o' => 0],
                    ['AND' => ['article_id' => 12, 'id' => 50]]
                ],
                [
                    'pp_articles_images',
                    ['o' => 1],
                    ['AND' => ['article_id' => 12, 'id' => 51]]
                ],
                [
                    'pp_articles_images',
                    ['o' => 2],
                    ['AND' => ['article_id' => 12, 'id' => 52]]
                ]
            )
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->saveGalleryOrder(12, '50;51;52');

        $this->assertTrue($result);
    }

    public function testSaveGalleryOrderSkipsEmptyValues(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_articles_images',
                ['o' => 0],
                ['AND' => ['article_id' => 7, 'id' => 99]]
            )
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->saveGalleryOrder(7, ';99;');

        $this->assertTrue($result);
    }

    public function testSaveFilesOrderUpdatesFilesOrder(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->exactly(3))
            ->method('update')
            ->withConsecutive(
                [
                    'pp_articles_files',
                    ['o' => 0],
                    ['AND' => ['article_id' => 12, 'id' => 70]]
                ],
                [
                    'pp_articles_files',
                    ['o' => 1],
                    ['AND' => ['article_id' => 12, 'id' => 71]]
                ],
                [
                    'pp_articles_files',
                    ['o' => 2],
                    ['AND' => ['article_id' => 12, 'id' => 72]]
                ]
            )
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->saveFilesOrder(12, '70;71;72');

        $this->assertTrue($result);
    }

    public function testSaveFilesOrderSkipsEmptyValues(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_articles_files',
                ['o' => 0],
                ['AND' => ['article_id' => 7, 'id' => 101]]
            )
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->saveFilesOrder(7, ';101;');

        $this->assertTrue($result);
    }

    public function testArchiveSetsStatusToMinusOne(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_articles', ['status' => -1], ['id' => 25])
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->archive(25);

        $this->assertTrue($result);
    }

    public function testArchiveReturnsFalseWhenUpdateFails(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_articles', ['status' => -1], ['id' => 999])
            ->willReturn(false);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->archive(999);

        $this->assertFalse($result);
    }

    public function testRestoreSetsStatusToZero(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_articles', ['status' => 0], ['id' => 25])
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $result = $repository->restore(25);

        $this->assertTrue($result);
    }

    public function testDeletePermanentlyRemovesArticleAndRelations(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $deleteCalls = [];

        $mockDb->expects($this->exactly(5))
            ->method('delete')
            ->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
                $deleteCalls[] = ['table' => $table, 'where' => $where];
                return true;
            });

        $repository = new ArticleRepository($mockDb);
        $result = $repository->deletePermanently(77);

        $this->assertTrue($result);
        $this->assertCount(5, $deleteCalls);
        $this->assertSame('pp_articles_pages', $deleteCalls[0]['table']);
        $this->assertSame('pp_articles_langs', $deleteCalls[1]['table']);
        $this->assertSame('pp_articles_images', $deleteCalls[2]['table']);
        $this->assertSame('pp_articles_files', $deleteCalls[3]['table']);
        $this->assertSame('pp_articles', $deleteCalls[4]['table']);
    }

    public function testPagesSummaryForArticlesBuildsLabels(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('query')
            ->willReturnCallback(function ($sql, $params = []) {
                return new class {
                    public function fetchAll()
                    {
                        return [
                            ['article_id' => 5, 'page_id' => 10, 'title' => 'Blog'],
                            ['article_id' => 5, 'page_id' => 11, 'title' => 'Poradniki'],
                            ['article_id' => 8, 'page_id' => 12, 'title' => 'Aktualnosci'],
                        ];
                    }
                };
            });

        $repository = new ArticleRepository($mockDb);
        $result = $repository->pagesSummaryForArticles([5, 8]);

        $this->assertSame(' - Blog / Poradniki', $result[5]);
        $this->assertSame(' - Aktualnosci', $result[8]);
    }

    public function testUpdateImageAltDelegatesToDatabase(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_articles_images', ['alt' => 'Nowy alt'], ['id' => 33])
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $this->assertTrue($repository->updateImageAlt(33, 'Nowy alt'));
    }

    public function testMarkFileToDeleteDelegatesToDatabase(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_articles_files', ['to_delete' => 1], ['id' => 17])
            ->willReturn(true);

        $repository = new ArticleRepository($mockDb);
        $this->assertTrue($repository->markFileToDelete(17));
    }

    public function testListArchivedForAdminWhitelistsSortAndDirection(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $queries = [];

        $mockDb->method('query')
            ->willReturnCallback(function ($sql, $params = []) use (&$queries) {
                $queries[] = ['sql' => $sql, 'params' => $params];

                if (strpos($sql, 'COUNT(0)') !== false) {
                    return new class {
                        public function fetchAll()
                        {
                            return [[1]];
                        }
                    };
                }

                return new class {
                    public function fetchAll()
                    {
                        return [[
                            'id' => 1,
                            'date_add' => '2020-01-01 00:00:00',
                            'date_modify' => '2020-01-01 00:00:00',
                            'title' => 'A',
                        ]];
                    }
                };
            });

        $repository = new ArticleRepository($mockDb);
        $repository->listArchivedForAdmin(
            [],
            'date_add DESC; DROP TABLE pp_articles; --',
            'DESC; DELETE FROM pp_users; --',
            1,
            100000
        );

        $this->assertCount(2, $queries);
        $dataSql = $queries[1]['sql'];

        $this->assertMatchesRegularExpression('/ORDER BY\s+pa\.date_add\s+DESC,\s+pa\.id\s+DESC/i', $dataSql);
        $this->assertStringNotContainsString('DROP TABLE', $dataSql);
        $this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql);
        $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
        $this->assertStringContainsString('pa.status = -1', $dataSql);
    }

    public function testListForAdminWhitelistsSortAndDirection(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $queries = [];

        $mockDb->method('query')
            ->willReturnCallback(function ($sql, $params = []) use (&$queries) {
                $queries[] = ['sql' => $sql, 'params' => $params];

                if (strpos($sql, 'COUNT(0)') !== false) {
                    return new class {
                        public function fetchAll()
                        {
                            return [[1]];
                        }
                    };
                }

                return new class {
                    public function fetchAll()
                    {
                        return [[
                            'id' => 1,
                            'date_add' => '2020-01-01 00:00:00',
                            'date_modify' => '2020-01-01 00:00:00',
                            'status' => 1,
                            'title' => 'A',
                            'user' => 'admin',
                        ]];
                    }
                };
            });

        $repository = new ArticleRepository($mockDb);
        $repository->listForAdmin(
            [],
            'date_add DESC; DROP TABLE pp_articles; --',
            'DESC; DELETE FROM pp_users; --',
            1,
            100000
        );

        $this->assertCount(2, $queries);
        $dataSql = $queries[1]['sql'];

        $this->assertMatchesRegularExpression('/ORDER BY\s+pa\.date_add\s+DESC,\s+pa\.id\s+DESC/i', $dataSql);
        $this->assertStringNotContainsString('DROP TABLE', $dataSql);
        $this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql);
        $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
    }

    // =========================================================================
    // FRONTEND METHODS
    // =========================================================================

    public function testArticleDetailsFrontendReturnsArticleWithRelations(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_articles', '*', ['id' => 5])
            ->willReturn(['id' => 5, 'status' => 1, 'show_title' => 1]);

        $mockDb->expects($this->exactly(4))
            ->method('select')
            ->willReturnOnConsecutiveCalls(
                [['lang_id' => 'pl', 'title' => 'Testowy', 'copy_from' => null]],
                [['id' => 10, 'src' => '/img/a.jpg']],
                [['id' => 20, 'src' => '/files/a.pdf']],
                [1, 2]
            );

        $repo = new ArticleRepository($mockDb);
        $article = $repo->articleDetailsFrontend(5, 'pl');

        $this->assertIsArray($article);
        $this->assertEquals(5, $article['id']);
        $this->assertArrayHasKey('language', $article);
        $this->assertEquals('Testowy', $article['language']['title']);
        $this->assertCount(1, $article['images']);
        $this->assertCount(1, $article['files']);
    }

    public function testArticleDetailsFrontendReturnsNullForMissing(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ArticleRepository($mockDb);
        $this->assertNull($repo->articleDetailsFrontend(999, 'pl'));
    }

    public function testArticleDetailsFrontendCopyFromFallback(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')
            ->willReturn(['id' => 7, 'status' => 1]);

        $mockDb->expects($this->exactly(5))
            ->method('select')
            ->willReturnOnConsecutiveCalls(
                // First call: langs with copy_from
                [['lang_id' => 'en', 'title' => 'English', 'copy_from' => 'pl']],
                // Second call: copy_from fallback
                [['lang_id' => 'pl', 'title' => 'Polski']],
                // images
                [],
                // files
                [],
                // pages
                []
            );

        $repo = new ArticleRepository($mockDb);
        $article = $repo->articleDetailsFrontend(7, 'en');

        $this->assertEquals('Polski', $article['language']['title']);
    }

    private function createFetchAllMock(array $data): object
    {
        return new class($data) {
            private $data;
            public function __construct($data) { $this->data = $data; }
            public function fetchAll($mode = null) { return $this->data; }
        };
    }

    public function testArticlesIdsReturnsSortedIds(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('query')
            ->willReturn($this->createFetchAllMock([
                ['id' => 3],
                ['id' => 7],
                ['id' => 1],
            ]));

        $repo = new ArticleRepository($mockDb);
        $result = $repo->articlesIds(1, 'pl', 10, 1, 0);

        $this->assertEquals([3, 7, 1], $result);
    }

    public function testArticlesIdsReturnsNullForEmpty(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('query')
            ->willReturn($this->createFetchAllMock([]));

        $repo = new ArticleRepository($mockDb);
        $this->assertNull($repo->articlesIds(1, 'pl', 10, 0, 0));
    }

    public function testPageArticlesCountReturnsInt(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('query')
            ->willReturn($this->createFetchAllMock([[12]]));

        $repo = new ArticleRepository($mockDb);
        $this->assertSame(12, $repo->pageArticlesCount(5, 'pl'));
    }

    public function testPageArticlesCountReturnsZeroForEmpty(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('query')
            ->willReturn($this->createFetchAllMock([]));

        $repo = new ArticleRepository($mockDb);
        $this->assertSame(0, $repo->pageArticlesCount(5, 'pl'));
    }

    public function testPageArticlesPagination(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        // pageArticlesCount query returns 25 articles
        // articlesIds query returns 10 article IDs
        $mockDb->method('query')
            ->willReturnOnConsecutiveCalls(
                $this->createFetchAllMock([[25]]),
                $this->createFetchAllMock([
                    ['id' => 11], ['id' => 12], ['id' => 13], ['id' => 14], ['id' => 15],
                    ['id' => 16], ['id' => 17], ['id' => 18], ['id' => 19], ['id' => 20],
                ])
            );

        $page = ['id' => 3, 'articles_limit' => 10, 'sort_type' => 1];
        $repo = new ArticleRepository($mockDb);
        $result = $repo->pageArticles($page, 'pl', 2);

        $this->assertArrayHasKey('articles', $result);
        $this->assertArrayHasKey('ls', $result);
        $this->assertSame(3, $result['ls']); // ceil(25/10) = 3
        $this->assertCount(10, $result['articles']);
    }

    public function testArticleNoindexReturnsBool(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_articles_langs', 'noindex', [
                'AND' => ['article_id' => 5, 'lang_id' => 'pl']
            ])
            ->willReturn(1);

        $repo = new ArticleRepository($mockDb);
        $this->assertTrue($repo->articleNoindex(5, 'pl'));
    }

    public function testArticleNoindexReturnsFalseForNonNoindex(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')
            ->willReturn(null);

        $repo = new ArticleRepository($mockDb);
        $this->assertFalse($repo->articleNoindex(5, 'pl'));
    }

    public function testNewsReturnsArticlesArray(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        // First get() for sort_type, then get() for article_details
        $mockDb->method('get')
            ->willReturnOnConsecutiveCalls(
                1, // sort_type
                ['id' => 10, 'status' => 1] // article data
            );

        // articlesIds query returns [10]
        $mockDb->method('query')
            ->willReturn($this->createFetchAllMock([['id' => 10]]));

        // article details selects
        $mockDb->method('select')
            ->willReturnOnConsecutiveCalls(
                [['lang_id' => 'pl', 'title' => 'News', 'copy_from' => null]],
                [],
                [],
                []
            );

        $repo = new ArticleRepository($mockDb);
        $result = $repo->news(3, 6, 'pl');

        $this->assertIsArray($result);
        $this->assertCount(1, $result);
        $this->assertEquals(10, $result[0]['id']);
    }

    public function testTopArticlesOrderByViews(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $queryCalls = 0;
        $mockDb->method('query')
            ->willReturnCallback(function ($sql) use (&$queryCalls) {
                $queryCalls++;
                if ($queryCalls === 1) {
                    $this->assertStringContainsString('views DESC', $sql);
                    return $this->createFetchAllMock([
                        ['id' => 5, 'date_add' => '2025-01-01', 'views' => 100, 'title' => 'Popular'],
                    ]);
                }
                return $this->createFetchAllMock([]);
            });

        $mockDb->method('get')
            ->willReturn(['id' => 5, 'status' => 1]);

        $mockDb->method('select')
            ->willReturnOnConsecutiveCalls(
                [['lang_id' => 'pl', 'title' => 'Popular', 'copy_from' => null]],
                [],
                [],
                []
            );

        $repo = new ArticleRepository($mockDb);
        $result = $repo->topArticles(3, 6, 'pl');

        $this->assertIsArray($result);
        $this->assertCount(1, $result);
    }

    public function testNewsListArticlesOrderByDateDesc(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $queryCalls = 0;
        $mockDb->method('query')
            ->willReturnCallback(function ($sql) use (&$queryCalls) {
                $queryCalls++;
                if ($queryCalls === 1) {
                    $this->assertStringContainsString('date_add DESC', $sql);
                    return $this->createFetchAllMock([
                        ['id' => 8, 'date_add' => '2025-06-15', 'title' => 'Newest'],
                    ]);
                }
                return $this->createFetchAllMock([]);
            });

        $mockDb->method('get')
            ->willReturn(['id' => 8, 'status' => 1]);

        $mockDb->method('select')
            ->willReturnOnConsecutiveCalls(
                [['lang_id' => 'pl', 'title' => 'Newest', 'copy_from' => null]],
                [],
                [],
                []
            );

        $repo = new ArticleRepository($mockDb);
        $result = $repo->newsListArticles(3, 6, 'pl');

        $this->assertIsArray($result);
        $this->assertCount(1, $result);
    }

    public function testListForAdminUsesBoundParamsForTitleFilter(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $queries = [];
        $attack = "' OR 1=1 --";

        $mockDb->method('query')
            ->willReturnCallback(function ($sql, $params = []) use (&$queries) {
                $queries[] = ['sql' => $sql, 'params' => $params];

                if (strpos($sql, 'COUNT(0)') !== false) {
                    return new class {
                        public function fetchAll()
                        {
                            return [[0]];
                        }
                    };
                }

                return new class {
                    public function fetchAll()
                    {
                        return [];
                    }
                };
            });

        $repository = new ArticleRepository($mockDb);
        $repository->listForAdmin(['title' => $attack], 'title', 'ASC', 1, 15);

        $this->assertCount(2, $queries);
        $countSql = $queries[0]['sql'];
        $countParams = $queries[0]['params'];

        $this->assertStringContainsString('LIKE :title', $countSql);
        $this->assertStringNotContainsString($attack, $countSql);
        $this->assertArrayHasKey(':title', $countParams);
        $this->assertSame('%' . $attack . '%', $countParams[':title']);
    }
}
