<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Tests\TestCase;

use App\Models\Tag;
use App\Models\User;
use App\Models\Role;
use App\Models\Blog;
use App\Models\Page;
use App\Models\PhotoBlock;
use App\Models\Photo;
use App\Models\FileUpload;
use App\Models\CourseList;
use App\Models\Livestream;
use App\Models\StaffProfile;

use Tests\Feature\SearchTestTrait;

class TagTest extends TestCase
{
    use SearchTestTrait;
    use WithFaker;

    protected function getClassname()
    {
        return 'tag';
    }

    public function test_tags_can_be_search_for_autocomplete()
    {
        $tag = Tag::factory()->create();

        $this->json('POST', route('tags.search'), [])
            ->assertStatus(401);

        $this->signIn(User::factory()->create());

        $this->json('POST', route('tags.search'), [])
            ->assertStatus(403);

        $this->signInAdmin();

        $this->json('POST', route('tags.search'), [])
            ->assertStatus(422)
            ->assertJsonValidationErrors([
                'terms',
            ]);

        $this->withoutExceptionHandling();
        $this->json('POST', route('tags.search'), ['terms' => strtolower(substr($tag->name, 0, 4))])
            ->assertSuccessful()
            ->assertJsonFragment([
                'id' => $tag->id,
                'name' => $tag->name,
            ]);
    }

    public function test_a_tag_can_be_created()
    {
        $input = Tag::factory()->raw();

        $this->json('POST', route('tags.store'), [])
             ->assertStatus(401);

        $this->signIn(User::factory()->create());

        $this->json('POST', route('tags.store'), [])
             ->assertStatus(403);

        $this->signInAdmin();

        $this->json('POST', route('tags.store'), [])
             ->assertStatus(422)
             ->assertJsonValidationErrors([
                'name',
             ]);

        $this->withoutExceptionHandling();
        $this->json('POST', route('tags.store'), $input)
             ->assertSuccessful()
             ->assertJsonFragment([
                'success' => Arr::get($input, 'name').' Saved',
             ]);

        $tag = Tag::all()->last();

        $this->assertInstanceOf(Tag::class, $tag);
        $this->assertEquals(Arr::get($input, 'name'), $tag->name);
    }

    public function test_a_tag_can_be_updated()
    {
        $tag = Tag::factory()->create();
        $input = Tag::factory()->raw();

        $this->json('POST', route('tags.update', ['id' => $tag->id]), [])
            ->assertStatus(401);

        $this->signIn(User::factory()->create());

        $this->json('POST', route('tags.update', ['id' => $tag->id]), [])
            ->assertStatus(403);

        $this->signInAdmin();

        $this->json('POST', route('tags.update', ['id' => $tag->id]), [])
             ->assertStatus(422)
             ->assertJsonValidationErrors([
                'name',
             ]);

        $this->json('POST', route('tags.update', ['id' => $tag->id]), $input)
             ->assertSuccessful()
             ->assertJsonFragment([
                'success' => Arr::get($input, 'name').' Saved',
             ]);

        $tag = Tag::all()->last();

        $this->assertInstanceOf(Tag::class, $tag);
        $this->assertEquals(Arr::get($input, 'name'), $tag->name);
    }

    public function test_the_tags_index_can_be_loaded()
    {
        $tag = Tag::factory()->create();

        $this->get(route('tags.index'))
             ->assertRedirect('/login');

        $this->signIn(User::factory()->create());

        $this->withoutExceptionHandling();
        $this->get(route('tags.index'))
             ->assertRedirect('/');

        $this->signInAdmin();

        $this->get(route('tags.index'))
             ->assertSuccessful();

        $this->json('GET', route('tags.index'))
            ->assertSuccessful();

        $this->assertTrue(auth()->user()->refresh()->editing);
    }

    public function test_creating_a_tag_that_has_a_parent()
    {
        $parent_tag = Tag::factory()->create();

        $this->assertNull($parent_tag->parent_tag_id);

        $input = Tag::factory()->raw([
            'parent_tag_id' => $parent_tag->id,
        ]);

        $this->signInAdmin();

        $this->json('POST', route('tags.store'), $input)
             ->assertSuccessful()
             ->assertJsonFragment([
                'success' => Arr::get($input, 'name').' Saved',
             ]);

        $tag = Tag::all()->last();
        $parent_tag->refresh();

        $this->assertInstanceOf(Tag::class, $tag);
        $this->assertEquals(Arr::get($input, 'name'), $tag->name);

        $this->assertInstanceOf(Tag::class, $tag->parentTag);
        $this->assertEquals($parent_tag->id, $tag->parentTag->id);
        $this->assertEquals(1, $parent_tag->tags->count());
        $this->assertTrue($parent_tag->tags->contains('id', $tag->id));
    }

    public function test_the_tags_index_loads_parents_and_children()
    {
        $parent_tag = Tag::factory()->create();
        $child_tag = Tag::factory()->create([
            'parent_tag_id' => $parent_tag->id,
        ]);

        $this->signInAdmin();

        $this->json('GET', route('tags.index'))
             ->assertSuccessful()
             ->assertJsonFragment([
                'name' => $parent_tag->name,
                'name' => $child_tag->name,
             ]);
    }

    public function test_only_parents_can_be_returned_from_a_search()
    {
        $name = $this->faker->name;
        $tag = Tag::factory()->create([
            'name' => $name,
        ]);

        $child = Tag::factory()->create([
            'name' => $name.'123',
            'parent_tag_id' => $tag->id,
        ]);

        $this->signInAdmin();

        $this->json('POST', route('tags.search'), ['terms' => strtolower(substr($tag->name, 0, 4))])
            ->assertSuccessful()
            ->assertJsonFragment([
                'name' => $tag->name,
                'name' => $child->name,
            ]);

        $this->json('POST', route('tags.search'), ['terms' => strtolower(substr($tag->name, 0, 4)), 'parents' => true])
            ->assertSuccessful()
            ->assertJsonFragment([
                'name' => $tag->name,
            ])
            ->assertJsonMissing([
                'name' => $child->name,
            ]);
    }

    public function test_managers_and_editors_can_search_for_tags()
    {
        $user = User::factory()->create();

        $roles = Role::where('name', 'LIKE', '%-manager%')
            ->orWhere('name', 'LIKE', '%-editor%')
            ->get();

        $tag = Tag::factory()->create();

        $this->signIn($user);

        foreach ($roles as $role) {
            $user->roles()->detach();
            $user->addRole($role);
            $user->refresh();

            $this->json('POST', route('tags.search'), ['terms' => strtolower(substr($tag->name, 0, 4))])
                ->assertSuccessful()
                ->assertJsonFragment([
                    'id' => $tag->id,
                    'name' => $tag->name,
                ]);
        }
    }

    public function test_a_tag_name_needs_to_be_unique()
    {
        $tag = Tag::factory()->create();

        $input = [
            'name' => $tag->name,
        ];

        $user = User::factory()->create();
        $user->addRole('tags-manager');
        $user->refresh();
        $this->signIn($user);

        $this->json('POST', route('tags.store'), $input)
             ->assertStatus(422)
             ->assertJsonValidationErrors([
                 'name',
             ]);
    }

    public function test_an_admin_user_can_create_a_tag_with_duplicate_name()
    {
        $tag = Tag::factory()->create();
        $parent_tag = Tag::factory()->create();

        $input = [
            'name' => $tag->name,
            'parent_tag_id' => $parent_tag->id,
        ];

        $this->withoutExceptionHandling();
        $this->signInAdmin();

        $this->json('POST', route('tags.store'), $input)
             ->assertSuccessful()
             ->assertJsonFragment([
                'success' => Arr::get($input, 'name').' Saved',
             ]);

        $tag = Tag::all()->last();

        $this->assertInstanceOf(Tag::class, $tag);
        $this->assertEquals(Arr::get($input, 'name'), $tag->name);
    }

    public function test_searching_for_a_tag_will_display_its_children()
    {
        $parent = Tag::factory()->create();
        $child = Tag::factory()->create([
            'parent_tag_id' => $parent->id,
        ]);

        $this->signInAdmin();

        $this->json('POST', route('tags.search'), ['terms' => strtolower(substr($parent->name, 0, 6))])
            ->assertSuccessful()
            ->assertJsonFragment([
                'name' => $parent->name,
                'name' => $child->name,
            ]);
    }

    public function test_having_a_dash_in_the_search_terms_still_returns_results()
    {
        $parent = Tag::factory()->create();
        $child = Tag::factory()->create([
            'parent_tag_id' => $parent->id,
        ]);

        $this->signInAdmin();

        $this->json('POST', route('tags.search'), ['terms' => strtolower($parent->name.' - ')])
            ->assertSuccessful()
            ->assertJsonFragment([
                'name' => $parent->name,
                'name' => $child->name,
            ]);
    }

    public function test_terms_shorter_than_3_characters_are_excluded()
    {
        $tag = Tag::factory()->create();
        $tag2 = Tag::factory()->create();

        $this->signInAdmin();

        $this->json('POST', route('tags.search'), ['terms' => strtolower($tag->name.' - '.substr($tag2->name, 0, 2))])
            ->assertSuccessful()
            ->assertJsonFragment([
                'name' => $tag->name,
            ])
            ->assertJsonMissing([
                'name' => $tag2->name,
            ]);
    }

    public function test_searching_for_a_parent_tag_and_child_tag_shows_both()
    {
        $parent = Tag::factory()->create();
        $child = Tag::factory()->create([
            'parent_tag_id' => $parent->id,
        ]);

        $this->signInAdmin();

        $this->json('POST', route('tags.search'), ['terms' => strtolower($parent->name.' - '.$child->name)])
            ->assertSuccessful()
            ->assertJsonFragment([
                'name' => $parent->name,
                'name' => $child->name,
            ]);
    }

    public function test_a_tags_items_can_be_loaded(): void
    {
        $tag = Tag::factory()->create();

        $this->json('POST', route('tags.items', ['id' => $tag->id]))
            ->assertStatus(401);

        $this->signIn(User::factory()->create());

        $this->json('POST', route('tags.items', ['id' => $tag->id]))
            ->assertStatus(403);

        $this->signInAdmin();
        $this->enableEditing();

        $content_element = $this->createContentElement(PhotoBlock::factory()->has(Photo::factory([
            'file_upload_id' => FileUpload::factory()->jpg(),
        ]), 'photos'));
        $content_element->addTag($tag);
        $photo_block = $content_element->content;
        $photo = $photo_block->photos()->first();
        $photo->addTag($tag);
        $page = $content_element->pages->first();
        $page->addTag($tag);
        $page->publish();

        $blog = Blog::factory()->create();
        $blog->addTag($tag);
        $blog->publish();

        $course_list = $this->createContentElement(CourseList::factory())->content;
        $this->assertInstanceOf(CourseList::class, $course_list);
        $course_list->addTag($tag);
        $course_list->contentElement->pages->first()->publish();
        $this->assertNotNull($course_list->contentElement->pages->first()->full_slug);

        $livestream = Livestream::factory()->create();
        $livestream->addTag($tag);
        $this->assertNotNull($livestream->full_slug);

        $staff_profile = StaffProfile::factory()->create();
        $staff_profile->addTag($tag);

        $tag->refresh();

        $this->assertNotNull($tag->items);
        $this->assertEquals(7, $tag->items->count());
        $this->assertNotNull($page->full_slug);
        $this->assertNotNull($blog->full_slug);

        $this->json('POST', route('tags.items', ['id' => $tag->id]))
             ->assertSuccessful()
             ->assertJsonFragment([
                'id' => $blog->id,
                'taggable_id' => $blog->id,
                'id' => $page->id,
                'taggable_id' => $page->id,
                'full_slug' => $page->full_slug,
                'full_slug' => $blog->full_slug,
                'taggable_id' => $content_element->id,
                'taggable_id' => $photo->id,
                'full_slug' => $course_list->contentElement->pages->first()->full_slug,
                'full_slug' => $livestream->full_slug,
                'full_slug' => 'staff-profiles/'.$staff_profile->id.'/edit',
             ]);
    }

    public function test_a_tag_can_remove_items_from_itself(): void
    {
        $tag = Tag::factory()->create();

        $blog = Blog::factory()->create();
        $content_element = $this->createContentElement(PhotoBlock::factory()->has(Photo::factory([
            'file_upload_id' => FileUpload::factory()->jpg(),
        ]), 'photos'));
        $photo_block = $content_element->content;
        $photo = $photo_block->photos()->first();
        $page = $content_element->pages->first();

        $content_element->addTag($tag);
        $page->addTag($tag);
        $photo->addTag($tag);
        $blog->addTag($tag);

        $input = [
            'items' => [
                ['taggable_id' => $page->id, 'taggable_type' => get_class($page)],
                ['taggable_id' => $photo->id, 'taggable_type' => get_class($photo)],
            ],
        ];

        $this->json('POST', route('tags.remove-items', ['id' => $tag->id]), $input)
            ->assertStatus(401);

        $this->signIn(User::factory()->create());

        $this->json('POST', route('tags.remove-items', ['id' => $tag->id]), $input)
            ->assertStatus(403);

        $this->signInAdmin();

        $this->assertEquals(4, $tag->items->count());

        $this->json('POST', route('tags.remove-items', ['id' => $tag->id]), [])
             ->assertStatus(422)
            ->assertJsonValidationErrors([
                'items',
            ]);

        $this->json('POST', route('tags.remove-items', ['id' => $tag->id]), $input)
             ->assertSuccessful()
            ->assertJsonFragment([
                'success' => '2 Items Removed',
            ]);

        $tag->refresh();

        $this->assertEquals(2, $tag->items->count());

        $this->assertTrue($tag->items->contains(function ($item) use ($content_element) {
            return get_class($item->taggable) === get_class($content_element) && $item->taggable->id === $content_element->id;
        }));

        $this->assertTrue($tag->items->contains(function ($item) use ($blog) {
            return get_class($item->taggable) === get_class($blog) && $item->taggable->id === $blog->id;
        }));

        $this->assertFalse($tag->items->contains(function ($item) use ($page) {
            return get_class($item->taggable) === get_class($page) && $item->taggable->id === $page->id;
        }));

        $this->assertFalse($tag->items->contains(function ($item) use ($photo) {
            return get_class($item->taggable) === get_class($photo) && $item->taggable->id === $photo->id;
        }));
    }

    public function test_a_tags_items_can_be_reassigned(): void
    {
        $tag = Tag::factory()->create();

        $blog = Blog::factory()->create();
        $content_element = $this->createContentElement(PhotoBlock::factory()->has(Photo::factory([
            'file_upload_id' => FileUpload::factory()->jpg(),
        ]), 'photos'));
        $photo_block = $content_element->content;
        $photo = $photo_block->photos()->first();
        $page = $content_element->pages->first();

        $content_element->addTag($tag);
        $page->addTag($tag);
        $photo->addTag($tag);
        $blog->addTag($tag);

        $new_tag = Tag::factory()->create();

        $input = [
            'items' => [
                ['taggable_id' => $page->id, 'taggable_type' => get_class($page)],
                ['taggable_id' => $photo->id, 'taggable_type' => get_class($photo)],
            ],
            'new_tag_id' => $new_tag->id,
        ];

        $this->json('POST', route('tags.retag-items', ['id' => $tag->id]), $input)
            ->assertStatus(401);

        $this->signIn(User::factory()->create());

        $this->json('POST', route('tags.retag-items', ['id' => $tag->id]), $input)
            ->assertStatus(403);

        $this->signInAdmin();

        $this->assertEquals(4, $tag->items->count());

        $this->json('POST', route('tags.retag-items', ['id' => $tag->id]), [])
             ->assertStatus(422)
            ->assertJsonValidationErrors([
                'items',
                'new_tag_id',
            ]);

        $this->json('POST', route('tags.retag-items', ['id' => $tag->id]), $input)
             ->assertSuccessful()
            ->assertJsonFragment([
                'success' => '2 Items Retagged',
            ]);

        $tag->refresh();
        $new_tag->refresh();

        $this->assertEquals(2, $tag->items()->count());
        $this->assertEquals(2, $new_tag->items()->count());

        $this->assertTrue($tag->items->contains(function ($item) use ($content_element) {
            return get_class($item->taggable) === get_class($content_element) && $item->taggable->id === $content_element->id;
        }));

        $this->assertTrue($tag->items->contains(function ($item) use ($blog) {
            return get_class($item->taggable) === get_class($blog) && $item->taggable->id === $blog->id;
        }));

        $this->assertFalse($tag->items->contains(function ($item) use ($page) {
            return get_class($item->taggable) === get_class($page) && $item->taggable->id === $page->id;
        }));

        $this->assertFalse($tag->items->contains(function ($item) use ($photo) {
            return get_class($item->taggable) === get_class($photo) && $item->taggable->id === $photo->id;
        }));

        $this->assertTrue($new_tag->items->contains(function ($item) use ($page) {
            return get_class($item->taggable) === get_class($page) && $item->taggable->id === $page->id;
        }));

        $this->assertTrue($new_tag->items->contains(function ($item) use ($photo) {
            return get_class($item->taggable) === get_class($photo) && $item->taggable->id === $photo->id;
        }));
    }
}
