<?php

namespace MintyPHP\Tests\Module\Bookmarks\Service;

use MintyPHP\Module\Bookmarks\Repository\BookmarkGroupRepository;
use MintyPHP\Module\Bookmarks\Repository\BookmarkNavigationRepository;
use MintyPHP\Module\Bookmarks\Repository\BookmarkRepository;
use MintyPHP\Module\Bookmarks\Service\BookmarkService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class BookmarkServiceTest extends TestCase
{
    private BookmarkRepository&MockObject $bookmarkRepo;
    private BookmarkGroupRepository&MockObject $groupRepo;
    private BookmarkNavigationRepository&MockObject $navigationRepo;
    private BookmarkService $service;

    protected function setUp(): void
    {
        $this->bookmarkRepo = $this->createMock(BookmarkRepository::class);
        $this->groupRepo = $this->createMock(BookmarkGroupRepository::class);
        $this->navigationRepo = $this->createMock(BookmarkNavigationRepository::class);
        $this->service = new BookmarkService($this->bookmarkRepo, $this->groupRepo, $this->navigationRepo);
    }

    public function testSaveBookmarkHappyPath(): void
    {
        $this->bookmarkRepo->method('countByUser')->willReturn(0);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(1);
        $this->bookmarkRepo->method('create')->willReturn(42);

        $result = $this->service->saveBookmark(1, 'Home', 'admin', null);

        $this->assertTrue($result['ok']);
        $this->assertSame('created', $result['mode']);
        $this->assertSame(42, $result['bookmark']['id']);
        $this->assertSame('Home', $result['bookmark']['name']);
        $this->assertSame('admin', $result['bookmark']['url']);
        $this->assertNull($result['bookmark']['group_id']);
        $this->assertArrayNotHasKey('icon', $result['bookmark']);
    }

    public function testSaveBookmarkUpsertsExistingBookmarkByExactUrl(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('findByUserAndUrl')->with(1, 'admin/users?a=1&b=2')->willReturn([
            'id' => 9,
            'url' => 'admin/users?a=1&b=2',
        ]);
        $this->bookmarkRepo->expects($this->once())->method('update')->with(
            9,
            1,
            [
                'name' => 'Users',
                'group_id' => null,
            ]
        )->willReturn(true);
        $this->bookmarkRepo->expects($this->never())->method('create');

        $result = $this->service->saveBookmark(1, 'Users', 'admin/users?b=2&a=1', null);

        $this->assertTrue($result['ok']);
        $this->assertSame('updated', $result['mode']);
        $this->assertSame(9, $result['bookmark']['id']);
        $this->assertSame('admin/users?a=1&b=2', $result['bookmark']['url']);
    }

    public function testSaveBookmarkUpsertsExistingBookmarkAndMovesToNewGroupAtEnd(): void
    {
        $this->groupRepo->expects($this->once())->method('findById')->with(3, 1)->willReturn([
            'id' => 3,
            'name' => 'Sales',
        ]);
        $this->bookmarkRepo->expects($this->once())->method('findByUserAndUrl')->with(1, 'admin/users')->willReturn([
            'id' => 9,
            'url' => 'admin/users',
            'group_id' => 1,
        ]);
        $this->bookmarkRepo->expects($this->once())->method('nextBookmarkSortOrder')->with(1, 3)->willReturn(12);
        $this->bookmarkRepo->expects($this->once())->method('update')->with(
            9,
            1,
            [
                'name' => 'Users',
                'group_id' => 3,
                'sort_order' => 12,
            ]
        )->willReturn(true);
        $this->bookmarkRepo->expects($this->never())->method('create');

        $result = $this->service->saveBookmark(1, 'Users', 'admin/users', 3);

        $this->assertTrue($result['ok']);
        $this->assertSame('updated', $result['mode']);
    }

    public function testSaveBookmarkCreatesWhenNoExactUrlMatch(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('findByUserAndUrl')->with(1, 'admin/users?a=1&b=2')->willReturn(false);
        $this->bookmarkRepo->method('countByUser')->willReturn(0);
        $this->navigationRepo->method('nextRootSortOrder')->willReturn(1);
        $this->bookmarkRepo->expects($this->once())->method('create')->willReturn(7);

        $result = $this->service->saveBookmark(1, 'Users', 'admin/users?a=1&b=2', null);

        $this->assertTrue($result['ok']);
        $this->assertSame('created', $result['mode']);
        $this->assertSame(7, $result['bookmark']['id']);
    }

    public function testSaveBookmarkRejectsEmptyName(): void
    {
        $result = $this->service->saveBookmark(1, '', 'admin', null);

        $this->assertFalse($result['ok']);
        $this->assertSame('name_required', $result['error']);
    }

    public function testSaveBookmarkRejectsEmptyUrl(): void
    {
        $result = $this->service->saveBookmark(1, 'Test', '', null);

        $this->assertFalse($result['ok']);
        $this->assertSame('url_required', $result['error']);
    }

    public function testSaveBookmarkRejectsAbsoluteUrl(): void
    {
        $result = $this->service->saveBookmark(1, 'External', 'https://evil.com', null);

        $this->assertFalse($result['ok']);
        $this->assertSame('url_required', $result['error']);
    }

    public function testSaveBookmarkRejectsProtocolRelativeUrl(): void
    {
        $result = $this->service->saveBookmark(1, 'External', '//evil.com', null);

        $this->assertFalse($result['ok']);
        $this->assertSame('url_required', $result['error']);
    }

    public function testSaveBookmarkMaxLimitReached(): void
    {
        $this->bookmarkRepo->method('countByUser')->willReturn(50);

        $result = $this->service->saveBookmark(1, 'Test', 'admin', null);

        $this->assertFalse($result['ok']);
        $this->assertSame('max_reached', $result['error']);
    }

    public function testSaveBookmarkWithGroupValidatesOwnership(): void
    {
        $this->bookmarkRepo->method('countByUser')->willReturn(0);
        $this->groupRepo->expects($this->any())->method('findById')->with(5, 1)->willReturn(false);

        $result = $this->service->saveBookmark(1, 'Test', 'admin', 5);

        $this->assertFalse($result['ok']);
        $this->assertSame('group_not_found', $result['error']);
    }

    public function testSaveBookmarkWithValidGroup(): void
    {
        $this->bookmarkRepo->method('countByUser')->willReturn(0);
        $this->bookmarkRepo->expects($this->once())->method('nextBookmarkSortOrder')->with(1, 5)->willReturn(7);
        $this->bookmarkRepo->expects($this->once())->method('create')->with([
            'user_id' => 1,
            'group_id' => 5,
            'name' => 'Test',
            'url' => 'admin',
            'sort_order' => 7,
        ])->willReturn(10);
        $this->groupRepo->expects($this->any())->method('findById')->with(5, 1)->willReturn(['id' => 5, 'name' => 'Dev']);

        $result = $this->service->saveBookmark(1, 'Test', 'admin', 5);

        $this->assertTrue($result['ok']);
        $this->assertSame(5, $result['bookmark']['group_id']);
    }

    public function testUpdateBookmarkNotFound(): void
    {
        $this->bookmarkRepo->method('findById')->willReturn(false);

        $result = $this->service->updateBookmark(1, 999, ['name' => 'New Name']);

        $this->assertFalse($result['ok']);
        $this->assertSame('not_found', $result['error']);
    }

    public function testUpdateBookmarkRejectsEmptyName(): void
    {
        $this->bookmarkRepo->method('findById')->willReturn(['id' => 1, 'name' => 'Old']);

        $result = $this->service->updateBookmark(1, 1, ['name' => '']);

        $this->assertFalse($result['ok']);
        $this->assertSame('name_required', $result['error']);
    }

    public function testUpdateBookmarkNoChanges(): void
    {
        $this->bookmarkRepo->method('findById')->willReturn(['id' => 1, 'name' => 'Old']);

        $result = $this->service->updateBookmark(1, 1, []);

        $this->assertTrue($result['ok']);
    }

    public function testUpdateBookmarkGroupChangeAppendsToTargetGroup(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('findById')->with(5, 1)->willReturn([
            'id' => 5,
            'name' => 'Users',
            'group_id' => 1,
        ]);
        $this->groupRepo->expects($this->once())->method('findById')->with(2, 1)->willReturn([
            'id' => 2,
            'name' => 'New',
        ]);
        $this->bookmarkRepo->expects($this->once())->method('nextBookmarkSortOrder')->with(1, 2)->willReturn(9);
        $this->bookmarkRepo->expects($this->once())->method('update')->with(5, 1, [
            'group_id' => 2,
            'sort_order' => 9,
        ])->willReturn(true);

        $result = $this->service->updateBookmark(1, 5, ['group_id' => 2]);

        $this->assertTrue($result['ok']);
    }

    public function testUpdateBookmarkMoveToUngroupedUsesRootSortOrder(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('findById')->with(5, 1)->willReturn([
            'id' => 5,
            'name' => 'Users',
            'group_id' => 2,
        ]);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(17);
        $this->bookmarkRepo->expects($this->once())->method('update')->with(5, 1, [
            'group_id' => null,
            'sort_order' => 17,
        ])->willReturn(true);

        $result = $this->service->updateBookmark(1, 5, ['group_id' => null]);

        $this->assertTrue($result['ok']);
    }

    public function testDeleteBookmark(): void
    {
        $this->bookmarkRepo->expects($this->any())->method('delete')->with(5, 1)->willReturn(true);

        $this->assertTrue($this->service->deleteBookmark(1, 5));
    }

    public function testSaveGroupHappyPath(): void
    {
        $this->groupRepo->method('countByUser')->willReturn(0);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(4);
        $this->groupRepo->expects($this->once())->method('create')->with([
            'user_id' => 1,
            'name' => 'Development',
            'icon' => 'bi-people',
            'sort_order' => 4,
        ])->willReturn(3);

        $result = $this->service->saveGroup(1, 'Development', 'bi-people');

        $this->assertTrue($result['ok']);
        $this->assertSame('created', $result['mode']);
        $this->assertSame(3, $result['group']['id']);
        $this->assertSame('Development', $result['group']['name']);
        $this->assertSame('bi-people', $result['group']['icon']);
    }

    public function testSaveGroupRejectsEmptyName(): void
    {
        $result = $this->service->saveGroup(1, '', 'bi-folder');

        $this->assertFalse($result['ok']);
        $this->assertSame('name_required', $result['error']);
    }

    public function testSaveGroupFallsBackToDefaultIcon(): void
    {
        $this->groupRepo->method('countByUser')->willReturn(0);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(1);
        $this->groupRepo->expects($this->once())->method('create')->with([
            'user_id' => 1,
            'name' => 'Development',
            'icon' => 'bi-folder',
            'sort_order' => 1,
        ])->willReturn(11);

        $result = $this->service->saveGroup(1, 'Development', 'invalid-icon');

        $this->assertTrue($result['ok']);
        $this->assertSame('bi-folder', $result['group']['icon']);
    }

    public function testSaveGroupMaxLimitReached(): void
    {
        $this->groupRepo->method('countByUser')->willReturn(10);

        $result = $this->service->saveGroup(1, 'New Group', 'bi-folder');

        $this->assertFalse($result['ok']);
        $this->assertSame('max_reached', $result['error']);
    }

    public function testSaveGroupRenameExisting(): void
    {
        $this->groupRepo->expects($this->any())->method('findById')->with(5, 1)->willReturn(['id' => 5, 'name' => 'Old']);
        $this->groupRepo->expects($this->once())
            ->method('update')
            ->with(5, 1, ['name' => 'Renamed', 'icon' => 'bi-heart'])
            ->willReturn(true);

        $result = $this->service->saveGroup(1, 'Renamed', 'bi-heart', 5);

        $this->assertTrue($result['ok']);
        $this->assertSame('updated', $result['mode']);
        $this->assertSame(5, $result['group']['id']);
        $this->assertSame('Renamed', $result['group']['name']);
        $this->assertSame('bi-heart', $result['group']['icon']);
    }

    public function testDeleteGroupUnsetsGroupAndDeletes(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('listByGroup')->with(1, 5)->willReturn([
            ['id' => 10, 'name' => 'A', 'url' => 'admin', 'group_id' => 5, 'sort_order' => 1],
            ['id' => 11, 'name' => 'B', 'url' => 'admin/users', 'group_id' => 5, 'sort_order' => 2],
        ]);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(20);
        $this->bookmarkRepo->expects($this->exactly(2))->method('unsetGroupId')
            ->willReturnCallback(fn (int $bmId, int $userId, int $sort) => match ([$bmId, $userId]) {
                [10, 1] => $sort === 20,
                [11, 1] => $sort === 21,
                default => false,
            });
        $this->groupRepo->expects($this->once())->method('delete')->with(5, 1)->willReturn(true);

        $this->assertTrue($this->service->deleteGroup(1, 5));
    }

    public function testDeleteGroupReturnsFalseWhenUnsetFails(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('listByGroup')->with(1, 5)->willReturn([
            ['id' => 10, 'name' => 'A', 'url' => 'admin', 'group_id' => 5, 'sort_order' => 1],
        ]);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(20);
        $this->bookmarkRepo->expects($this->once())->method('unsetGroupId')->with(10, 1, 20)->willReturn(false);
        $this->groupRepo->expects($this->never())->method('delete');

        $this->assertFalse($this->service->deleteGroup(1, 5));
    }

    public function testDeleteGroupWithNoBookmarksDeletesDirectly(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('listByGroup')->with(1, 5)->willReturn([]);
        $this->navigationRepo->expects($this->once())->method('nextRootSortOrder')->with(1)->willReturn(1);
        $this->bookmarkRepo->expects($this->never())->method('unsetGroupId');
        $this->groupRepo->expects($this->once())->method('delete')->with(5, 1)->willReturn(true);

        $this->assertTrue($this->service->deleteGroup(1, 5));
    }

    public function testReorderGroupsDelegatesToRepository(): void
    {
        $pairs = [
            ['id' => 4, 'sort_order' => 1],
            ['id' => 6, 'sort_order' => 2],
        ];
        $this->groupRepo->expects($this->once())->method('updateSortOrders')->with(1, $pairs)->willReturn(true);

        $this->assertTrue($this->service->reorderGroups(1, $pairs));
    }

    public function testReorderBookmarksReturnsFalseOnRepositoryFailure(): void
    {
        $pairs = [
            ['id' => 11, 'sort_order' => 1],
            ['id' => 7, 'sort_order' => 2],
        ];
        $this->bookmarkRepo->expects($this->once())->method('updateSortOrders')->with(1, $pairs)->willReturn(false);

        $this->assertFalse($this->service->reorderBookmarks(1, $pairs));
    }

    public function testReorderRootDelegatesToNavigationRepository(): void
    {
        $pairs = [
            ['kind' => 'bookmark', 'id' => 11, 'sort_order' => 1],
            ['kind' => 'group', 'id' => 2, 'sort_order' => 2],
        ];
        $this->navigationRepo->expects($this->once())
            ->method('updateRootSortOrders')
            ->with(1, $pairs)
            ->willReturn(true);

        $this->assertTrue($this->service->reorderRoot(1, $pairs));
    }

    public function testListGroupedForUser(): void
    {
        $this->groupRepo->method('listByUser')->willReturn([
            ['id' => 1, 'name' => 'Dev', 'icon' => 'bi-folder', 'sort_order' => 0],
        ]);
        $this->bookmarkRepo->method('listByUser')->willReturn([
            ['id' => 10, 'name' => 'Home', 'url' => 'admin', 'group_id' => null, 'sort_order' => 0],
            ['id' => 11, 'name' => 'Users', 'url' => 'admin/users', 'group_id' => 1, 'sort_order' => 0],
        ]);

        $result = $this->service->listGroupedForUser(1);

        $this->assertCount(1, $result['groups']);
        $this->assertSame('Dev', $result['groups'][0]['name']);
        $this->assertSame('bi-folder', $result['groups'][0]['icon']);
        $this->assertCount(1, $result['groups'][0]['bookmarks']);
        $this->assertSame('Users', $result['groups'][0]['bookmarks'][0]['name']);
        $this->assertArrayNotHasKey('icon', $result['groups'][0]['bookmarks'][0]);
        $this->assertCount(1, $result['ungrouped']);
        $this->assertSame('Home', $result['ungrouped'][0]['name']);
        $this->assertArrayNotHasKey('icon', $result['ungrouped'][0]);
    }

    public function testReorderFromItemsRejectsInvalidType(): void
    {
        $result = $this->service->reorderFromItems(1, 'invalid', []);

        $this->assertFalse($result['ok']);
        $this->assertSame('invalid_type', $result['error']);
    }

    public function testReorderFromItemsRejectsEmptyItems(): void
    {
        $result = $this->service->reorderFromItems(1, 'bookmarks', []);

        $this->assertFalse($result['ok']);
        $this->assertSame('invalid_items', $result['error']);
    }

    public function testReorderFromItemsBookmarksHappyPath(): void
    {
        $this->bookmarkRepo->expects($this->once())->method('updateSortOrders')->with(1, [
            ['id' => 3, 'sort_order' => 1],
            ['id' => 7, 'sort_order' => 2],
        ])->willReturn(true);

        $result = $this->service->reorderFromItems(1, 'bookmarks', [
            ['id' => 3, 'sort_order' => 1],
            ['id' => 7, 'sort_order' => 2],
        ]);

        $this->assertTrue($result['ok']);
    }

    public function testReorderRootFromItemsHappyPath(): void
    {
        $this->navigationRepo->expects($this->once())->method('updateRootSortOrders')->with(1, [
            ['kind' => 'group', 'id' => 2, 'sort_order' => 1],
            ['kind' => 'bookmark', 'id' => 5, 'sort_order' => 2],
        ])->willReturn(true);

        $result = $this->service->reorderRootFromItems(1, [
            ['kind' => 'group', 'id' => 2, 'sort_order' => 1],
            ['kind' => 'bookmark', 'id' => 5, 'sort_order' => 2],
        ]);

        $this->assertTrue($result['ok']);
    }

    public function testReorderRootFromItemsRejectsEmptyItems(): void
    {
        $result = $this->service->reorderRootFromItems(1, []);

        $this->assertFalse($result['ok']);
        $this->assertSame('invalid_items', $result['error']);
    }

    public function testAllowedGroupIconsReturnsNonEmptyList(): void
    {
        $icons = BookmarkService::allowedGroupIcons();
        $this->assertNotEmpty($icons);
        $this->assertContains('bi-folder', $icons);
        $this->assertContains('bi-house', $icons);
    }
}
