<?php

namespace Tests\Helpers;

use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;

/**
 * Class to provider and action entity models for common test case
 * operations. Tracks handled models and only returns fresh models.
 * Does not dedupe against nested/child/parent models.
 */
class EntityProvider
{
    /**
     * @var array<string, int[]>
     */
    protected array $fetchCache = [
        'book' => [],
        'page' => [],
        'bookshelf' => [],
        'chapter' => [],
    ];

    /**
     * Get an unfetched page from the system.
     */
    public function page(callable|null $queryFilter = null): Page
    {
        /** @var Page $page */
        $page = Page::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['page'])->first();
        $this->addToCache($page);
        return $page;
    }

    public function pageWithinChapter(): Page
    {
        return $this->page(fn(Builder $query) => $query->whereHas('chapter')->with('chapter'));
    }

    public function pageNotWithinChapter(): Page
    {
        return $this->page(fn(Builder $query) => $query->whereNull('chapter_id'));
    }

    public function templatePage(): Page
    {
        $page = $this->page();
        $page->template = true;
        $page->save();

        return $page;
    }

    /**
     * Get an unfetched chapter from the system.
     */
    public function chapter(callable|null $queryFilter = null): Chapter
    {
        /** @var Chapter $chapter */
        $chapter = Chapter::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['chapter'])->first();
        $this->addToCache($chapter);
        return $chapter;
    }

    public function chapterHasPages(): Chapter
    {
        return $this->chapter(fn(Builder $query) => $query->whereHas('pages'));
    }

    /**
     * Get an unfetched book from the system.
     */
    public function book(callable|null $queryFilter = null): Book
    {
        /** @var Book $book */
        $book = Book::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['book'])->first();
        $this->addToCache($book);
        return $book;
    }

    /**
     * Get a book that has chapters and pages assigned.
     */
    public function bookHasChaptersAndPages(): Book
    {
        return $this->book(function (Builder $query) {
            $query->has('chapters')->has('pages')->with(['chapters', 'pages']);
        });
    }

    /**
     * Get an unfetched shelf from the system.
     */
    public function shelf(callable|null $queryFilter = null): Bookshelf
    {
        /** @var Bookshelf $shelf */
        $shelf = Bookshelf::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['bookshelf'])->first();
        $this->addToCache($shelf);
        return $shelf;
    }

    /**
     * Get a shelf that has books assigned.
     */
    public function shelfHasBooks(): Bookshelf
    {
        return $this->shelf(fn(Builder $query) => $query->whereHas('books'));
    }

    /**
     * Get all entity types from the system.
     * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf}
     */
    public function all(): array
    {
        return [
            'page'      => $this->page(),
            'chapter'   => $this->chapter(),
            'book'      => $this->book(),
            'bookshelf' => $this->shelf(),
        ];
    }

    public function updatePage(Page $page, array $data): Page
    {
        $this->addToCache($page);
        return app()->make(PageRepo::class)->update($page, $data);
    }

    /**
     * Create a book to page chain of entities that belong to a specific user.
     * @return array{book: Book, chapter: Chapter, page: Page}
     */
    public function createChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array
    {
        if (empty($updaterUser)) {
            $updaterUser = $creatorUser;
        }

        $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id];
        /** @var Book $book */
        $book = Book::factory()->create($userAttrs);
        $chapter = Chapter::factory()->create(array_merge(['book_id' => $book->id], $userAttrs));
        $page = Page::factory()->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs));

        $book->rebuildPermissions();
        $this->addToCache([$page, $chapter, $book]);

        return compact('book', 'chapter', 'page');
    }

    /**
     * Create and return a new bookshelf.
     */
    public function newShelf(array $input = ['name' => 'test shelf', 'description' => 'My new test shelf']): Bookshelf
    {
        $shelf = app(BookshelfRepo::class)->create($input, []);
        $this->addToCache($shelf);
        return $shelf;
    }

    /**
     * Create and return a new book.
     */
    public function newBook(array $input = ['name' => 'test book', 'description' => 'My new test book']): Book
    {
        $book = app(BookRepo::class)->create($input);
        $this->addToCache($book);
        return $book;
    }

    /**
     * Create and return a new test chapter.
     */
    public function newChapter(array $input, Book $book): Chapter
    {
        $chapter = app(ChapterRepo::class)->create($input, $book);
        $this->addToCache($chapter);
        return $chapter;
    }

    /**
     * Create and return a new test page.
     */
    public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page
    {
        $book = $this->book();
        $pageRepo = app(PageRepo::class);
        $draftPage = $pageRepo->getNewDraftPage($book);
        $this->addToCache($draftPage);
        return $pageRepo->publishDraft($draftPage, $input);
    }

    /**
     * Create and return a new test draft page.
     */
    public function newDraftPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page
    {
        $book = $this->book();
        $pageRepo = app(PageRepo::class);
        $draftPage = $pageRepo->getNewDraftPage($book);
        $pageRepo->updatePageDraft($draftPage, $input);
        $this->addToCache($draftPage);
        return $draftPage;
    }

    /**
     * Send an entity to the recycle bin.
     */
    public function sendToRecycleBin(Entity $entity)
    {
        $trash = app()->make(TrashCan::class);

        if ($entity instanceof Page) {
            $trash->softDestroyPage($entity);
        } elseif ($entity instanceof Chapter) {
            $trash->softDestroyChapter($entity);
        } elseif ($entity instanceof Book) {
            $trash->softDestroyBook($entity);
        } elseif ($entity instanceof Bookshelf) {
            $trash->softDestroyBookshelf($entity);
        }

        $entity->refresh();
        if (is_null($entity->deleted_at)) {
            throw new \Exception("Could not send entity type [{$entity->getMorphClass()}] to the recycle bin");
        }
    }

    /**
     * Fully destroy the given entity from the system, bypassing the recycle bin
     * stage. Still runs through main app deletion logic.
     */
    public function destroy(Entity $entity)
    {
        $trash = app()->make(TrashCan::class);
        $trash->destroyEntity($entity);
    }

    /**
     * @param Entity|Entity[] $entities
     */
    protected function addToCache($entities): void
    {
        if (!is_array($entities)) {
            $entities = [$entities];
        }

        foreach ($entities as $entity) {
            $this->fetchCache[$entity->getType()][] = $entity->id;
        }
    }
}
