<?php

namespace Tests\Feature;

use App\Models\User;
use App\Providers\AppServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Tests\TestCase;

/**
 * A chat list shows your chats, wherever it is rendered.
 *
 * PR #27 added two new places that list conversations, neither scoped to the
 * user: the sidebar view composer, which renders on every page in the shell,
 * and the /chats index with its two counts. Both sat behind auth, which is not
 * the same as being behind ownership.
 *
 * The rule has to hold in every one of them at once. Scoping the list while
 * leaving the URL open, or one list and not another, is worse than scoping
 * none: it looks fixed exactly where somebody would check.
 */
class ChatListIsScopedTest extends TestCase
{
    use RefreshDatabase;

    private function conversationFor(?User $user, string $title, ?string $chatType = null): string
    {
        $id = (string) Str::uuid();

        DB::table('agent_conversations')->insert([
            'id' => $id,
            'participant_type' => $user?->getMorphClass(),
            'participant_id' => $user?->getKey(),
            'chat_type' => $chatType,
            'title' => $title,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return $id;
    }

    /** The /chats index lists mine and not theirs. */
    public function test_the_chats_index_lists_only_my_conversations(): void
    {
        $mine = User::factory()->create();
        $theirs = User::factory()->create();

        $this->conversationFor($mine, 'My winter sale');
        $this->conversationFor($theirs, 'Their secret launch');

        $this->actingAs($mine)
            ->get(route('chats.index'))
            ->assertOk()
            ->assertSee('My winter sale')
            ->assertDontSee('Their secret launch');
    }

    /**
     * And its counts agree with the list.
     *
     * Three separate queries had to remember the same two conditions, so the
     * count could say five while the list showed one.
     */
    public function test_the_chats_index_counts_only_my_conversations(): void
    {
        $mine = User::factory()->create();
        $theirs = User::factory()->create();

        $this->conversationFor($mine, 'Mine one');
        $this->conversationFor($theirs, 'Theirs one');
        $this->conversationFor($theirs, 'Theirs two');

        $response = $this->actingAs($mine)->get(route('chats.index'));

        $stats = $response->viewData('stats');

        $this->assertSame(1, $stats['all'], 'The count includes other people\'s chats.');
        $this->assertSame(1, $stats['today']);
    }

    /**
     * The sidebar is the one that renders everywhere.
     *
     * Checked on the dashboard rather than the chat page on purpose: the chat
     * controllers pass their own scoped list, so the composer only fills the
     * gap on pages like this one, which is exactly where it went unnoticed.
     */
    public function test_the_sidebar_on_other_pages_lists_only_my_conversations(): void
    {
        $mine = User::factory()->create();
        $theirs = User::factory()->create();

        $this->conversationFor($mine, 'My winter sale');
        $this->conversationFor($theirs, 'Their secret launch');

        $this->actingAs($mine)
            ->get(route('home'))
            ->assertOk()
            ->assertSee('My winter sale')
            ->assertDontSee('Their secret launch');
    }

    /**
     * A conversation nobody owns stays visible.
     *
     * These predate ownership being tracked. mayOpen() lets them through, so
     * the lists must too, or the list hides what the link still opens.
     */
    public function test_a_conversation_with_no_participant_is_still_listed(): void
    {
        $mine = User::factory()->create();

        $this->conversationFor(null, 'Legacy conversation');

        $this->actingAs($mine)
            ->get(route('chats.index'))
            ->assertOk()
            ->assertSee('Legacy conversation');
    }

    /**
     * The Google chat's own list is filtered on the value the column holds.
     *
     * The composer asked for chat_type 'google'; nothing writes that. It was
     * unreachable rather than broken - GoogleChatController passes its own list, so
     * the composer's Google branch never decides anything on a googlechat route.
     *
     * Which is why the first version of this test read the file for a substring: a
     * request-level test cannot reach the branch. That is a real difficulty and a
     * bad answer - a string assertion passes when the string is in a comment,
     * passes when the query is deleted, and says nothing about which conversations
     * come back. Proven bad, too: putting the original defect back leaves a
     * source-matching version green only if the string happens to change, and
     * leaves a request-level version green always.
     *
     * So the branch is exercised where it lives, with the route resolved to a
     * googlechat one. The value being pinned is 'google_ads', and this fails if it
     * goes back to 'google'.
     */
    public function test_the_google_sidebar_filter_uses_the_value_the_column_holds(): void
    {
        $user = User::factory()->create();

        $wanted = $this->conversationFor($user, 'A Google Ads chat', 'google_ads');
        $wrongValue = $this->conversationFor($user, 'Filtered on a value nothing writes', 'google');
        $ordinary = $this->conversationFor($user, 'An ordinary chat');

        $listed = $this->composerListFor('googlechat.show', $user);

        $this->assertContains($wanted, $listed, "a 'google_ads' conversation is missing from the Google sidebar");
        $this->assertNotContains($wrongValue, $listed, "'google' is not a value anything writes");
        $this->assertNotContains($ordinary, $listed, 'an ordinary chat leaked into the Google sidebar');
    }

    /**
     * And on an ordinary page the sidebar excludes the Google ones.
     *
     * Reachable through a request, unlike the branch above: the dashboard renders
     * the shell without supplying its own list, which is the case the composer
     * exists for and where it went unnoticed.
     */
    public function test_the_sidebar_on_an_ordinary_page_excludes_google_chats(): void
    {
        $user = User::factory()->create();

        $this->conversationFor($user, 'My winter sale');
        $this->conversationFor($user, 'A Google Ads chat', 'google_ads');

        $this->actingAs($user)
            ->get(route('home'))
            ->assertOk()
            ->assertSee('My winter sale')
            ->assertDontSee('A Google Ads chat');
    }

    /**
     * What the sidebar composer itself would list, on a given named route.
     *
     * The route is resolved onto the current request rather than visited, because
     * visiting a googlechat route lets its controller supply the list and the
     * composer's answer is discarded. This is the branch under test, so this is the
     * only place it can be observed.
     *
     * @return list<string>
     */
    private function composerListFor(string $routeName, User $user): array
    {
        $this->actingAs($user);

        $route = Route::getRoutes()->getByName($routeName);

        $this->assertNotNull($route, "The route {$routeName} does not exist.");

        request()->setRouteResolver(fn () => $route);

        $method = new \ReflectionMethod(AppServiceProvider::class, 'sidebarConversations');

        return collect($method->invoke(app(AppServiceProvider::class, ['app' => app()])))
            ->pluck('id')
            ->map(strval(...))
            ->all();
    }
}
