<?php

declare(strict_types=1);

use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\Message;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;

uses(RefreshDatabase::class);

// ── Helpers ──────────────────────────────────────────────────────────────────

function makeUser(array $attrs = []): User
{
    return User::factory()->create(array_merge(['is_active' => 1], $attrs));
}

function actingAs(User $user): User
{
    return $user;           // alias — call $this->actingAs() in HTTP tests
}

// ── 1. Direct conversation creation ──────────────────────────────────────────

test('user can create a direct conversation', function () {
    $userA = makeUser();
    $userB = makeUser();

    $response = $this->actingAs($userA)->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => $userB->id,
        'body'         => 'Hello there!',
    ]);

    $response->assertStatus(201);

    expect(Conversation::count())->toBe(1)
        ->and(ConversationParticipant::count())->toBe(2)
        ->and(Message::count())->toBe(1);

    $conv = Conversation::first();
    expect($conv->type)->toBe('direct')
        ->and($conv->last_message_at)->not->toBeNull();
});

test('sending a second message to same user reuses existing conversation', function () {
    $userA = makeUser();
    $userB = makeUser();

    $this->actingAs($userA)->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => $userB->id,
        'body'         => 'Message 1',
    ])->assertStatus(201);

    $this->actingAs($userA)->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => $userB->id,
        'body'         => 'Message 2',
    ])->assertStatus(201);

    expect(Conversation::count())->toBe(1)
        ->and(Message::count())->toBe(2);
});

test('user cannot message themselves', function () {
    $user = makeUser();

    $this->actingAs($user)->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => $user->id,
        'body'         => 'Hello me!',
    ])->assertStatus(422);
});

// ── 2. Group conversation creation ───────────────────────────────────────────

test('user can create a group conversation', function () {
    $creator = makeUser();
    $memberA = makeUser();
    $memberB = makeUser();

    $response = $this->actingAs($creator)->postJson('/api/messages/conversation', [
        'type'            => 'group',
        'title'           => 'Test Group',
        'participant_ids' => [$memberA->id, $memberB->id],
        'body'            => 'Welcome everyone!',
    ]);

    $response->assertStatus(201);

    expect(Conversation::count())->toBe(1)
        ->and(ConversationParticipant::count())->toBe(3)    // creator + 2
        ->and(Message::count())->toBe(1);

    $conv = Conversation::first();
    expect($conv->type)->toBe('group')
        ->and($conv->title)->toBe('Test Group');
});

// ── 3. Add / remove participant ───────────────────────────────────────────────

test('group admin can add a participant', function () {
    $creator  = makeUser();
    $existing = makeUser();
    $newGuy   = makeUser();

    $conv = Conversation::create(['type' => 'group', 'title' => 'G', 'created_by' => $creator->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $creator->id,  'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $existing->id, 'role' => 'member', 'joined_at' => now()],
    ]);

    $response = $this->actingAs($creator)->postJson("/api/messages/{$conv->id}/add-user", [
        'user_id' => $newGuy->id,
    ]);

    $response->assertStatus(200);
    expect(ConversationParticipant::where('conversation_id', $conv->id)->whereNull('left_at')->count())->toBe(3);
});

test('non-admin cannot add a participant', function () {
    $creator = makeUser();
    $member  = makeUser();
    $newGuy  = makeUser();

    $conv = Conversation::create(['type' => 'group', 'title' => 'G', 'created_by' => $creator->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $creator->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $member->id,  'role' => 'member', 'joined_at' => now()],
    ]);

    $this->actingAs($member)->postJson("/api/messages/{$conv->id}/add-user", [
        'user_id' => $newGuy->id,
    ])->assertStatus(403);
});

test('admin can remove a participant', function () {
    $creator = makeUser();
    $member  = makeUser();

    $conv = Conversation::create(['type' => 'group', 'title' => 'G', 'created_by' => $creator->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $creator->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $member->id,  'role' => 'member', 'joined_at' => now()],
    ]);

    $this->actingAs($creator)->deleteJson("/api/messages/{$conv->id}/remove-user", [
        'user_id' => $member->id,
    ])->assertStatus(200);

    expect(
        ConversationParticipant::where('conversation_id', $conv->id)
            ->where('user_id', $member->id)
            ->whereNotNull('left_at')
            ->exists()
    )->toBeTrue();
});

// ── 4. Leave conversation ─────────────────────────────────────────────────────

test('member can leave a group', function () {
    $creator = makeUser();
    $member  = makeUser();

    $conv = Conversation::create(['type' => 'group', 'title' => 'G', 'created_by' => $creator->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $creator->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $member->id,  'role' => 'member', 'joined_at' => now()],
    ]);

    $this->actingAs($member)->deleteJson("/api/messages/{$conv->id}/leave")
        ->assertStatus(200);

    expect(
        ConversationParticipant::where('conversation_id', $conv->id)
            ->where('user_id', $member->id)
            ->whereNotNull('left_at')
            ->exists()
    )->toBeTrue();
});

test('last admin leaving promotes another member to admin', function () {
    $creator = makeUser();
    $member  = makeUser();

    $conv = Conversation::create(['type' => 'group', 'title' => 'G', 'created_by' => $creator->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $creator->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $member->id,  'role' => 'member', 'joined_at' => now()],
    ]);

    $this->actingAs($creator)->deleteJson("/api/messages/{$conv->id}/leave")
        ->assertStatus(200);

    $promoted = ConversationParticipant::where('conversation_id', $conv->id)
        ->where('user_id', $member->id)
        ->first();

    expect($promoted->role)->toBe('admin');
});

// ── 5. Unread logic ───────────────────────────────────────────────────────────

test('unread count is computed correctly', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin',  'joined_at' => now(), 'last_read_at' => null],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now(), 'last_read_at' => now()],
    ]);

    // userB sends 3 messages
    Message::insert([
        ['conversation_id' => $conv->id, 'sender_id' => $userB->id, 'body' => 'Hi',  'created_at' => now(), 'updated_at' => now()],
        ['conversation_id' => $conv->id, 'sender_id' => $userB->id, 'body' => 'Hey', 'created_at' => now(), 'updated_at' => now()],
        ['conversation_id' => $conv->id, 'sender_id' => $userB->id, 'body' => '!!',  'created_at' => now(), 'updated_at' => now()],
    ]);

    // userA has last_read_at = null → all 3 messages are unread
    expect($conv->unreadCountFor($userA->id))->toBe(3);

    // userB sent the messages; unread for userB = 0
    expect($conv->unreadCountFor($userB->id))->toBe(0);
});

test('mark-as-read endpoint sets last_read_at', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin',  'joined_at' => now(), 'last_read_at' => null],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now(), 'last_read_at' => null],
    ]);

    $this->actingAs($userA)->postJson("/api/messages/{$conv->id}/read")
        ->assertStatus(200);

    expect(
        ConversationParticipant::where('conversation_id', $conv->id)
            ->where('user_id', $userA->id)
            ->whereNotNull('last_read_at')
            ->exists()
    )->toBeTrue();
});

// ── 6. Privacy rule enforcement ───────────────────────────────────────────────

test('user with privacy=nobody cannot be messaged', function () {
    $sender    = makeUser();
    $recipient = makeUser(['allow_messages_from' => 'nobody']);

    $this->actingAs($sender)->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => $recipient->id,
        'body'         => 'Can I message you?',
    ])->assertStatus(403);
});

test('user with privacy=everyone can be messaged by anyone', function () {
    $sender    = makeUser();
    $recipient = makeUser(['allow_messages_from' => 'everyone']);

    $this->actingAs($sender)->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => $recipient->id,
        'body'         => 'Hello!',
    ])->assertStatus(201);
});

// ── 7. Message pagination ─────────────────────────────────────────────────────

test('message list is cursor paginated', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now()],
    ]);

    // Insert 50 messages
    $rows = [];
    for ($i = 1; $i <= 50; $i++) {
        $rows[] = [
            'conversation_id' => $conv->id,
            'sender_id'       => $userA->id,
            'body'            => "Message {$i}",
            'created_at'      => now()->addSeconds($i),
            'updated_at'      => now()->addSeconds($i),
        ];
    }
    Message::insert($rows);

    $response = $this->actingAs($userA)->getJson("/api/messages/{$conv->id}");
    $response->assertStatus(200);

    $data = $response->json();
    expect($data['data'])->toHaveCount(30)
        ->and($data['next_cursor'])->not->toBeNull();

    $cursorResponse = $this->actingAs($userA)->getJson("/api/messages/{$conv->id}?cursor={$data['next_cursor']}");
    $cursorResponse->assertStatus(200);

    $olderData = $cursorResponse->json();
    expect($olderData['data'])->toHaveCount(20)
        ->and($olderData['next_cursor'])->toBeNull();
});

test('sending a message creates notifications for active unmuted participants', function () {
    $sender = makeUser();
    $recipient = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $sender->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $sender->id, 'role' => 'admin', 'joined_at' => now(), 'is_muted' => false, 'is_archived' => false],
        ['conversation_id' => $conv->id, 'user_id' => $recipient->id, 'role' => 'member', 'joined_at' => now(), 'is_muted' => false, 'is_archived' => false],
    ]);

    $this->actingAs($sender)->postJson("/api/messages/{$conv->id}", [
        'body' => 'Hello with notify',
    ])->assertStatus(201);

    $row = DB::table('notifications')
        ->where('user_id', $recipient->id)
        ->where('type', 'message')
        ->latest('id')
        ->first();

    expect($row)->not->toBeNull();
});

test('sending a message does not notify recipient who disallows sender by privacy setting', function () {
    $sender = makeUser();
    $recipient = makeUser(['allow_messages_from' => 'nobody']);

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $sender->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $sender->id, 'role' => 'admin', 'joined_at' => now(), 'is_muted' => false, 'is_archived' => false],
        ['conversation_id' => $conv->id, 'user_id' => $recipient->id, 'role' => 'member', 'joined_at' => now(), 'is_muted' => false, 'is_archived' => false],
    ]);

    $this->actingAs($sender)->postJson("/api/messages/{$conv->id}", [
        'body' => 'Privacy should block notification',
    ])->assertStatus(201);

    $count = DB::table('notifications')
        ->where('user_id', $recipient->id)
        ->where('type', 'message')
        ->count();

    expect($count)->toBe(0);
});

// ── 8. Message editing ────────────────────────────────────────────────────────

test('user can edit their own message', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now()],
    ]);

    $message = Message::create([
        'conversation_id' => $conv->id,
        'sender_id'       => $userA->id,
        'body'            => 'Original body',
    ]);

    $response = $this->actingAs($userA)->patchJson("/api/messages/message/{$message->id}", [
        'body' => 'Updated body',
    ]);

    $response->assertStatus(200)
        ->assertJsonFragment(['body' => 'Updated body']);

    expect($message->fresh()->edited_at)->not->toBeNull();
});

test('user cannot edit another users message', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now()],
    ]);

    $message = Message::create([
        'conversation_id' => $conv->id,
        'sender_id'       => $userA->id,
        'body'            => 'Original body',
    ]);

    $this->actingAs($userB)->patchJson("/api/messages/message/{$message->id}", [
        'body' => 'Hacked!',
    ])->assertStatus(403);
});

// ── 9. Message soft-delete ────────────────────────────────────────────────────

test('user can delete their own message', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin',  'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now()],
    ]);

    $message = Message::create([
        'conversation_id' => $conv->id,
        'sender_id'       => $userA->id,
        'body'            => 'Delete me',
    ]);

    $this->actingAs($userA)->deleteJson("/api/messages/message/{$message->id}")
        ->assertStatus(200);

    expect(Message::withTrashed()->find($message->id)->deleted_at)->not->toBeNull();
});

// ── 10. N+1 query check ──────────────────────────────────────────────────────

test('conversation list does not produce N+1 queries', function () {
    $user  = makeUser();
    $other = makeUser();

    // Create 5 conversations
    for ($i = 0; $i < 5; $i++) {
        $conv = Conversation::create(['type' => 'direct', 'created_by' => $user->id]);
        ConversationParticipant::insert([
            ['conversation_id' => $conv->id, 'user_id' => $user->id,  'role' => 'admin',  'joined_at' => now()],
            ['conversation_id' => $conv->id, 'user_id' => $other->id, 'role' => 'member', 'joined_at' => now()],
        ]);
        Message::create(['conversation_id' => $conv->id, 'sender_id' => $other->id, 'body' => "Hi #{$i}"]);
        $conv->update(['last_message_at' => now()]);
    }

    $queryCount = 0;
    \Illuminate\Support\Facades\DB::listen(function () use (&$queryCount) { $queryCount++; });

    $response = $this->actingAs($user)->getJson('/api/messages/conversations');
    $response->assertStatus(200);

    // Expect a small fixed number of queries regardless of conversation count.
    // A naive implementation would run 1 + N*3 queries (N+1). Reasonable bound: <= 25.
    expect($queryCount)->toBeLessThanOrEqual(25);
});

// ── 11. Messaging settings ────────────────────────────────────────────────────

test('user can update their messaging privacy setting', function () {
    $user = makeUser(['allow_messages_from' => 'everyone']);

    $this->actingAs($user)->patchJson('/api/messages/settings', [
        'allow_messages_from' => 'followers',
    ])->assertStatus(200)
      ->assertJsonFragment(['allow_messages_from' => 'followers']);

    expect($user->fresh()->allow_messages_from)->toBe('followers');
});

test('invalid privacy value is rejected', function () {
    $user = makeUser();

    $this->actingAs($user)->patchJson('/api/messages/settings', [
        'allow_messages_from' => 'friends_only',
    ])->assertStatus(422);
});

test('mark-as-read invalidates cached conversation list unread count', function () {
    $userA = makeUser();
    $userB = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $userA->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $userA->id, 'role' => 'admin', 'joined_at' => now(), 'last_read_at' => null],
        ['conversation_id' => $conv->id, 'user_id' => $userB->id, 'role' => 'member', 'joined_at' => now(), 'last_read_at' => now()],
    ]);

    Message::create([
        'conversation_id' => $conv->id,
        'sender_id' => $userB->id,
        'body' => 'Unread for userA',
    ]);

    $firstList = $this->actingAs($userA)->getJson('/api/messages/conversations');
    $firstList->assertStatus(200);
    expect((int) $firstList->json('data.0.unread_count'))->toBe(1);

    $this->actingAs($userA)->postJson("/api/messages/{$conv->id}/read")
        ->assertStatus(200);

    $secondList = $this->actingAs($userA)->getJson('/api/messages/conversations');
    $secondList->assertStatus(200);
    expect((int) $secondList->json('data.0.unread_count'))->toBe(0);
});

test('message send endpoint enforces per-user rate limit', function () {
    $sender = makeUser();
    $recipient = makeUser();

    $conv = Conversation::create(['type' => 'direct', 'created_by' => $sender->id]);
    ConversationParticipant::insert([
        ['conversation_id' => $conv->id, 'user_id' => $sender->id, 'role' => 'admin', 'joined_at' => now()],
        ['conversation_id' => $conv->id, 'user_id' => $recipient->id, 'role' => 'member', 'joined_at' => now()],
    ]);

    for ($i = 1; $i <= 20; $i++) {
        $this->actingAs($sender)->postJson("/api/messages/{$conv->id}", [
            'body' => "Burst {$i}",
        ])->assertStatus(201);
    }

    $this->actingAs($sender)->postJson("/api/messages/{$conv->id}", [
        'body' => 'Burst 21',
    ])->assertStatus(429);
});

// ── 12. Unauthenticated access is blocked ────────────────────────────────────

test('conversations list requires authentication', function () {
    $this->getJson('/api/messages/conversations')
        ->assertStatus(401);
});

test('sending a message requires authentication', function () {
    $this->postJson('/api/messages/conversation', [
        'type'         => 'direct',
        'recipient_id' => 1,
        'body'         => 'Hello',
    ])->assertStatus(401);
});
