<?php

/**
 * 文件功能：聊天室控制器功能测试
 *
 * 覆盖进房、发言、图片消息、权限限制与广播范围等关键聊天流程。
 */

namespace Tests\Feature;

use App\Events\MessageSent;
use App\Models\Department;
use App\Models\Gift;
use App\Models\Position;
use App\Models\Room;
use App\Models\Sysparam;
use App\Models\User;
use App\Models\UserPosition;
use App\Models\VipLevel;
use App\Support\PositionPermissionRegistry;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;

/**
 * 类功能：验证聊天室核心控制器的关键行为。
 */
class ChatControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * 每个测试前清空 Redis，避免跨用例污染在线状态与消息缓存。
     */
    protected function setUp(): void
    {
        parent::setUp();
        Redis::flushall();
    }

    /**
     * 测试用户可以正常进入聊天室页面。
     */
    public function test_can_view_room(): void
    {
        $room = Room::create(['room_name' => 'testroom']);
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertStatus(200);
        $response->assertViewIs('chat.frame');

        // Assert user was added to room in redis
        $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username));
    }

    /**
     * 测试关闭房间会拒绝普通用户直接进入。
     */
    public function test_cannot_view_closed_room_without_bypass_permission(): void
    {
        $room = Room::create([
            'room_name' => 'closed',
            'door_open' => false,
        ]);
        $user = User::factory()->create([
            'user_level' => 1,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertForbidden();
        $this->assertSame(0, Redis::hexists("room:{$room->id}:users", $user->username));
    }

    /**
     * 测试等级不足的用户不能进入受限房间。
     */
    public function test_cannot_view_room_when_level_is_below_permit_level(): void
    {
        $room = Room::create([
            'room_name' => 'viprm',
            'permit_level' => 10,
            'door_open' => true,
        ]);
        $user = User::factory()->create([
            'user_level' => 5,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertForbidden();
        $this->assertSame(0, Redis::hexists("room:{$room->id}:users", $user->username));
    }

    /**
     * 测试只有已成功进入房间的用户才可通过 Presence 频道鉴权。
     */
    public function test_room_presence_channel_requires_room_access_and_join_state(): void
    {
        $room = Room::create([
            'room_name' => 'guard',
            'door_open' => true,
        ]);
        $user = User::factory()->create();
        $channelCallback = Broadcast::driver()->getChannels()->get('room.{roomId}');

        $this->assertIsCallable($channelCallback);
        $this->assertFalse($channelCallback($user, (string) $room->id));

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $authorizedPayload = $channelCallback($user, (string) $room->id);
        $this->assertIsArray($authorizedPayload);
        $this->assertSame($user->id, $authorizedPayload['id'] ?? null);

        Redis::del("room:{$room->id}:users");
        $this->assertFalse($channelCallback($user, (string) $room->id));
    }

    /**
     * 测试聊天室 Presence 频道会返回当前在职职务图标与名称。
     */
    public function test_room_presence_channel_returns_active_position_payload(): void
    {
        $room = Room::create([
            'room_name' => 'posbadge',
            'door_open' => true,
        ]);
        $user = $this->createUserWithPositionPermissions([]);
        $position = $user->activePosition()->with('position.department')->first()?->position;
        $channelCallback = Broadcast::driver()->getChannels()->get('room.{roomId}');

        $this->assertIsCallable($channelCallback);

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $authorizedPayload = $channelCallback($user, (string) $room->id);

        $this->assertIsArray($authorizedPayload);
        $this->assertSame('🛡️', $authorizedPayload['position_icon'] ?? null);
        $this->assertSame($position?->name, $authorizedPayload['position_name'] ?? null);
        $this->assertSame($position?->department?->name, $authorizedPayload['department_name'] ?? null);
    }

    /**
     * 测试手动存点接口会返回部门、职务与会员展示字段，供前端复用自动存点格式。
     */
    public function test_heartbeat_returns_identity_summary_for_save_notice(): void
    {
        Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '0']);
        Sysparam::updateOrCreate(['alias' => 'jjb_per_heartbeat'], ['body' => '0']);
        Sysparam::updateOrCreate(['alias' => 'auto_event_chance'], ['body' => '0']);
        Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
        Cache::flush();

        $room = Room::create([
            'room_name' => 'hbident',
            'door_open' => true,
        ]);
        $vipLevel = VipLevel::factory()->create([
            'name' => '至尊会员',
            'icon' => '👑',
        ]);
        $user = User::factory()->create([
            'user_level' => 100,
            'exp_num' => 168762,
            'jjb' => 1100017,
            'vip_level_id' => $vipLevel->id,
            'hy_time' => now()->addDay(),
        ]);
        $department = Department::create([
            'name' => '办公厅',
            'rank' => 100,
            'color' => '#1d4ed8',
            'sort_order' => 1,
        ]);
        $position = Position::create([
            'department_id' => $department->id,
            'name' => '厅长',
            'icon' => '🏛️',
            'rank' => 100,
            'level' => 100,
            'sort_order' => 1,
        ]);
        UserPosition::create([
            'user_id' => $user->id,
            'position_id' => $position->id,
            'appointed_at' => now(),
            'is_active' => true,
        ]);

        $response = $this->actingAs($user)->postJson(route('chat.heartbeat', $room->id));

        $response->assertOk();
        $response->assertJsonPath('data.identity_summary', '部门 办公厅 · 职务 🏛️ 厅长 · 会员 👑 至尊会员');
        $response->assertJsonPath('data.department_name', '办公厅');
        $response->assertJsonPath('data.position_name', '厅长');
        $response->assertJsonPath('data.vip_name', '至尊会员');
        $response->assertJsonPath('data.vip_icon', '👑');
    }

    /**
     * 测试聊天室 Presence 频道会返回仍在有效期内的当日状态载荷。
     */
    public function test_room_presence_channel_returns_active_daily_status_payload(): void
    {
        $room = Room::create([
            'room_name' => 'dsbadge',
            'door_open' => true,
        ]);
        $user = User::factory()->create([
            'daily_status_key' => 'working_hard',
            'daily_status_expires_at' => now()->addHours(3),
        ]);
        $channelCallback = Broadcast::driver()->getChannels()->get('room.{roomId}');

        $this->assertIsCallable($channelCallback);

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $authorizedPayload = $channelCallback($user, (string) $room->id);

        $this->assertIsArray($authorizedPayload);
        $this->assertSame('working_hard', $authorizedPayload['daily_status_key'] ?? null);
        $this->assertSame('搬砖', $authorizedPayload['daily_status_label'] ?? null);
        $this->assertSame('🧱', $authorizedPayload['daily_status_icon'] ?? null);
        $this->assertSame('工作学习', $authorizedPayload['daily_status_group'] ?? null);
        $this->assertSame(
            $user->fresh()->daily_status_expires_at?->toIso8601String(),
            $authorizedPayload['daily_status_expires_at'] ?? null
        );
    }

    /**
     * 测试主干默认聊天室页面不会渲染虚拟形象挂载点和配置。
     */
    public function test_room_view_does_not_render_avatar_widget_or_config_by_default(): void
    {
        $room = Room::create(['room_name' => 'avguard']);
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertDontSee('chat-avatar-widget');
        $response->assertDontSee('chatAvatarWidget');
    }

    /**
     * 测试聊天室输入区会渲染系统播报屏蔽按钮与对应选项。
     */
    public function test_room_view_renders_block_system_sender_controls(): void
    {
        $room = Room::create(['room_name' => 'blockmenu']);
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertSee('🔕 屏蔽', false);
        $response->assertSee('🔇 禁音', false);
        $response->assertSee('钓鱼播报');
        $response->assertSee('星海小博士');
        $response->assertSee('百家乐');
        $response->assertSee('跑马');
        $response->assertSee('神秘箱子');
        $response->assertSee('chat_blocked_system_senders');
        $response->assertSee('toggleBlockedSystemSender');
    }

    /**
     * 测试聊天室输入区会渲染状态入口，并移除旧的直接清屏按钮绑定。
     */
    public function test_room_view_renders_daily_status_button_and_hides_direct_local_clear_button(): void
    {
        $room = Room::create(['room_name' => 'statusmenu']);
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertSee('data-chat-feature-menu-toggle', false);
        $response->assertSee('功能');
        $response->assertSee('设个状态', false);
        $response->assertSee('data-chat-daily-status-open', false);
        $response->assertSee('data-chat-feature-shortcut="shop"', false);
        $response->assertSee('data-chat-feature-shortcut="vip"', false);
        $response->assertSee('data-chat-feature-shortcut="game"', false);
        $response->assertSee('data-chat-feature-shortcut="avatar"', false);
        $response->assertSee('data-chat-feature-shortcut="bank"', false);
        $response->assertSee('data-chat-feature-shortcut="marriage"', false);
        $response->assertSee('data-chat-feature-shortcut="friend"', false);
        $response->assertSee('data-chat-feature-shortcut="settings"', false);
        $response->assertSee('本地清屏', false);
        $response->assertDontSee('onclick="toggleFeatureMenu(event)"', false);
        $response->assertDontSee('onclick="localClearScreen()"', false);
    }

    /**
     * 测试无聊天室权限的职务用户看不到顶部管理按钮。
     */
    public function test_room_view_hides_admin_menu_for_position_without_room_permissions(): void
    {
        $room = Room::create(['room_name' => 'nomenu']);
        $user = $this->createUserWithPositionPermissions([]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertDontSee('🛠 管理', false);
        $response->assertDontSee('🪧 设公告', false);
    }

    /**
     * 测试只授予公告权限时，顶部管理菜单仅显示对应按钮。
     */
    public function test_room_view_renders_only_granted_room_management_buttons(): void
    {
        $room = Room::create(['room_name' => 'annmenu']);
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertSee('🛠 管理', false);
        $response->assertSee('🪧 设公告', false);
        $response->assertDontSee("runAdminAction('announce-message')", false);
        $response->assertDontSee("selectEffect('fireworks')", false);
    }

    /**
     * 测试仅有全屏特效权限时，只显示特效分组。
     */
    public function test_room_view_renders_fullscreen_effect_group_only_when_permission_exists(): void
    {
        $room = Room::create(['room_name' => 'effectmenu']);
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_FULLSCREEN_EFFECT,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertSee('🛠 管理', false);
        $response->assertSee('全屏特效');
        $response->assertSee('🎆 烟花', false);
        $response->assertDontSee("runAdminAction('announcement')", false);
        $response->assertDontSee("runAdminAction('announce-message')", false);
    }

    /**
     * 测试用户名片中的用户管理按钮会按职务权限显示。
     */
    public function test_room_view_renders_only_granted_user_management_buttons(): void
    {
        $room = Room::create(['room_name' => 'usermenu']);
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::USER_WARN,
            PositionPermissionRegistry::USER_MUTE,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertSee('⚠️ 警告', false);
        $response->assertSee('🔇 禁言', false);
        $response->assertDontSee('🚫 踢出', false);
        $response->assertDontSee('🧊 冻结', false);
    }

    /**
     * 测试没有用户管理权限时，用户名片中不会渲染相关按钮。
     */
    public function test_room_view_hides_user_management_buttons_without_permissions(): void
    {
        $room = Room::create(['room_name' => 'nousermenu']);
        $user = $this->createUserWithPositionPermissions([]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertDontSee('⚠️ 警告', false);
        $response->assertDontSee('🚫 踢出', false);
        $response->assertDontSee('🔇 禁言', false);
        $response->assertDontSee('🧊 冻结', false);
    }

    /**
     * 测试站长拥有在职职务权限时能看到管理菜单中的刷新全员按钮。
     */
    public function test_room_view_shows_refresh_all_button_for_positioned_site_owner(): void
    {
        $room = Room::create(['room_name' => 'owner-rf']);
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_CLEAR_SCREEN,
        ], ['id' => 1, 'user_level' => 100]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertSee('🛠 管理', false);
        $response->assertSee('data-chat-admin-action="refresh-all"', false);
        $response->assertSee('♻️ 刷新全员', false);
    }

    /**
     * 测试普通职务用户不会看到刷新全员按钮。
     */
    public function test_room_view_hides_refresh_all_button_for_non_site_owner(): void
    {
        $room = Room::create(['room_name' => 'normal-rf']);
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertOk();
        $response->assertDontSee("runAdminAction('refresh-all')", false);
    }

    /**
     * 测试用户可以发送普通文本消息。
     */
    public function test_can_send_message(): void
    {
        $room = Room::create(['room_name' => 'test_send']);
        $user = User::factory()->create();

        // 进房
        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '测试消息',
            'is_secret' => false,
            'font_color' => '#000000',
            'action' => '微笑',
        ]);

        $response->assertStatus(200);
        $response->assertJson(['status' => 'success']);

        // 查看 Redis 里的消息记录
        $messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
        $this->assertNotEmpty($messages);

        $found = false;
        foreach ($messages as $msgJson) {
            $msg = json_decode($msgJson, true);
            if ($msg['from_user'] === $user->username && $msg['content'] === '测试消息') {
                $found = true;
                break;
            }
        }
        $this->assertTrue($found, 'Message not found in Redis');
    }

    /**
     * 测试未进入当前房间时不能直接调用发言接口。
     */
    public function test_send_message_requires_user_to_be_in_room(): void
    {
        $room = Room::create(['room_name' => 'sndbd']);
        $user = User::factory()->create();

        $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '跨房间发言',
            'is_secret' => false,
            'font_color' => '#000000',
            'action' => '',
        ]);

        $response->assertStatus(403);
        $this->assertSame([], Redis::lrange("room:{$room->id}:messages", 0, -1));
    }

    /**
     * 测试发送接口会拦截不在白名单内的危险动作值。
     */
    public function test_send_message_rejects_invalid_action_value(): void
    {
        $room = Room::create(['room_name' => 'badact']);
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '危险动作测试',
            'is_secret' => false,
            'font_color' => '#000000',
            'action' => '\"></span><img src=x onerror=alert(1)>',
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('action');
    }

    /**
     * 测试发送接口会拒绝非标准十六进制颜色，避免 style 属性注入。
     */
    public function test_send_message_rejects_invalid_font_color(): void
    {
        $room = Room::create(['room_name' => 'badcolor']);
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '颜色注入测试',
            'is_secret' => false,
            'font_color' => '#fff;evil',
            'action' => '',
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('font_color');
    }

    /**
     * 测试合法发言颜色仍可正常保存到消息与用户偏好。
     */
    public function test_send_message_accepts_valid_hex_font_color(): void
    {
        $room = Room::create(['room_name' => 'goodcolor']);
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '合法颜色测试',
            'is_secret' => false,
            'font_color' => '#12AaF0',
            'action' => '',
        ]);

        $response->assertOk()->assertJson(['status' => 'success']);

        $messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
        $message = collect($messages)
            ->map(fn (string $item) => json_decode($item, true))
            ->first(fn (array $item) => ($item['content'] ?? null) === '合法颜色测试');

        $this->assertSame('#12AaF0', $message['font_color'] ?? null);
        $this->assertSame('#12AaF0', $user->fresh()->s_color);
    }

    /**
     * 测试定向消息仅广播到发送方与接收方私有频道。
     */
    public function test_targeted_message_event_uses_private_user_channels(): void
    {
        $sender = User::factory()->create(['username' => 'sender-user']);
        $receiver = User::factory()->create(['username' => 'receiver-user']);

        $event = new MessageSent(1, [
            'room_id' => 1,
            'from_user' => $sender->username,
            'to_user' => $receiver->username,
            'content' => '只给你看',
            'is_secret' => true,
            'font_color' => '#000000',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ]);

        $channels = $event->broadcastOn();

        $this->assertCount(2, $channels);
        $this->assertContainsOnlyInstancesOf(PrivateChannel::class, $channels);
        $this->assertSame([
            'private-user.'.$sender->id,
            'private-user.'.$receiver->id,
        ], array_map(fn (PrivateChannel $channel) => $channel->name, $channels));
    }

    /**
     * 测试公共消息仍广播到房间 Presence 频道。
     */
    public function test_public_message_event_still_uses_room_presence_channel(): void
    {
        $event = new MessageSent(3, [
            'room_id' => 3,
            'from_user' => 'tester',
            'to_user' => '大家',
            'content' => '公开消息',
            'is_secret' => false,
            'font_color' => '#000000',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ]);

        $channels = $event->broadcastOn();

        $this->assertCount(1, $channels);
        $this->assertInstanceOf(PresenceChannel::class, $channels[0]);
        $this->assertSame('presence-room.3', $channels[0]->name);
    }

    /**
     * 测试文本内容为字符串 0 时仍可正常发送。
     */
    public function test_can_send_zero_message_content(): void
    {
        $room = Room::create(['room_name' => 'send0']);
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '0',
            'is_secret' => false,
            'font_color' => '#000000',
            'action' => '',
        ]);

        $response->assertOk();
        $response->assertJson(['status' => 'success']);

        $messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
        $found = false;

        foreach ($messages as $msgJson) {
            $msg = json_decode($msgJson, true);

            if ($msg['from_user'] === $user->username && $msg['content'] === '0') {
                $found = true;
                break;
            }
        }

        $this->assertTrue($found, 'Zero message not found in Redis');
    }

    /**
     * 测试用户可以发送带缩略图的图片消息。
     */
    public function test_can_send_image_message(): void
    {
        Storage::fake('public');

        $room = Room::create(['room_name' => 'imgsend']);
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->post(route('chat.send', $room->id), [
            'to_user' => '大家',
            'content' => '图片说明',
            'font_color' => '#000000',
            'action' => '',
            'image' => UploadedFile::fake()->image('chat-picture.png', 1280, 960),
        ], [
            'Accept' => 'application/json',
        ]);

        $response->assertOk();
        $response->assertJson(['status' => 'success']);

        $messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
        $payload = collect($messages)
            ->map(fn (string $item) => json_decode($item, true))
            ->first(fn (array $item) => ($item['from_user'] ?? null) === $user->username && ($item['message_type'] ?? null) === 'image');

        $this->assertNotNull($payload);
        $this->assertSame('image', $payload['message_type'] ?? null);
        $this->assertNotEmpty($payload['image_path'] ?? null);
        $this->assertNotEmpty($payload['image_thumb_path'] ?? null);
        $this->assertNotEmpty($payload['image_url'] ?? null);
        $this->assertNotEmpty($payload['image_thumb_url'] ?? null);
        Storage::disk('public')->assertExists($payload['image_path']);
        Storage::disk('public')->assertExists($payload['image_thumb_path']);
    }

    /**
     * 测试聊天室发送接口在 419 场景下会返回稳定的 JSON 提示。
     */
    public function test_chat_send_http_419_exception_renders_json_response(): void
    {
        $request = Request::create('/room/1/send', 'POST', server: [
            'HTTP_ACCEPT' => 'application/json',
        ]);

        $response = $this->app->make(\Illuminate\Contracts\Debug\ExceptionHandler::class)
            ->render($request, new HttpException(419, 'Page Expired'));

        \Illuminate\Testing\TestResponse::fromBaseResponse($response)->assertStatus(419)->assertJson([
            'status' => 'error',
            'message' => '页面已过期，请刷新后重试。',
        ]);
    }

    /**
     * 测试房间公告更新广播中的动态内容会被转义。
     */
    public function test_set_announcement_escapes_html_in_system_message(): void
    {
        $room = Room::create(['room_name' => 'annsafe']);
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
        ]);

        $this->actingAs($user)->get(route('chat.room', $room->id));

        $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
            'announcement' => '<img src=x onerror=alert(1)>',
        ]);

        $response->assertOk();

        $messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
        $systemMessage = collect($messages)
            ->map(fn (string $item) => json_decode($item, true))
            ->first(fn (array $item) => ($item['from_user'] ?? null) === '系统公告');

        $this->assertNotNull($systemMessage);
        $this->assertStringNotContainsString('<img src=x onerror=alert(1)>', (string) $systemMessage['content']);
        $this->assertStringContainsString('&lt;img src=x onerror=alert(1)&gt;', (string) $systemMessage['content']);
    }

    /**
     * 测试超过保留期的聊天图片会被命令清理并改成过期占位消息。
     */
    public function test_purge_command_cleans_expired_chat_images(): void
    {
        Storage::fake('public');

        Storage::disk('public')->put('chat-images/2026-04-08/sample_original.png', 'original');
        Storage::disk('public')->put('chat-images/2026-04-08/sample_thumb.png', 'thumb');

        $message = \App\Models\Message::create([
            'room_id' => 1,
            'from_user' => 'tester',
            'to_user' => '大家',
            'content' => '历史图片',
            'is_secret' => false,
            'font_color' => '#000000',
            'action' => '',
            'message_type' => 'image',
            'image_path' => 'chat-images/2026-04-08/sample_original.png',
            'image_thumb_path' => 'chat-images/2026-04-08/sample_thumb.png',
            'image_original_name' => 'sample.png',
            'sent_at' => now()->subDays(4),
        ]);

        $this->artisan('messages:purge', [
            '--days' => 30,
            '--image-days' => 3,
        ])->assertExitCode(0);

        $message->refresh();

        $this->assertSame('expired_image', $message->message_type);
        $this->assertNull($message->image_path);
        $this->assertNull($message->image_thumb_path);
        $this->assertNull($message->image_original_name);
        $this->assertStringContainsString('图片已过期', $message->content);
        Storage::disk('public')->assertMissing('chat-images/2026-04-08/sample_original.png');
        Storage::disk('public')->assertMissing('chat-images/2026-04-08/sample_thumb.png');
    }

    /**
     * 测试心跳接口可以正常返回成功响应。
     */
    public function test_can_trigger_heartbeat()
    {
        $room = Room::create(['room_name' => 'test_hb']);
        $user = User::factory()->create(['exp_num' => 0]);

        $response = $this->actingAs($user)->postJson(route('chat.heartbeat', $room->id));

        $response->assertStatus(200);
        $response->assertJsonFragment(['status' => 'success']);

        $user->refresh();
        $this->assertGreaterThanOrEqual(0, $user->exp_num); // Might be 1 depending on sysparam
    }

    /**
     * 测试显式退房会清理 Redis 在线状态。
     */
    public function test_can_leave_room()
    {
        $room = Room::create(['room_name' => 'test_leave']);
        $user = User::factory()->create();

        // 进房
        $this->actingAs($user)->get(route('chat.room', $room->id));
        $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username));

        // 显式退房
        $response = $this->actingAs($user)->postJson(route('chat.leave', $room->id).'?explicit=1');

        $response->assertStatus(200);

        // 缓存中被移除
        $this->assertEquals(0, Redis::hexists("room:{$room->id}:users", $user->username));
    }

    /**
     * 测试签名退房链接同样可以正常清理在线状态。
     */
    public function test_can_leave_room_through_signed_expired_route(): void
    {
        $room = Room::create(['room_name' => 'leave2']);
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('chat.room', $room->id));
        $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username));

        $url = URL::temporarySignedRoute('chat.leave.expired', now()->addMinutes(5), [
            'id' => $room->id,
            'user' => $user->id,
        ]);

        $response = $this->getJson($url);

        $response->assertStatus(200);
        $this->assertEquals(0, Redis::hexists("room:{$room->id}:users", $user->username));
    }

    /**
     * 测试双击名片赠金币会写入私聊消息，且只有赠送双方能看到历史记录。
     */
    public function test_gift_gold_creates_secret_message_visible_only_to_participants(): void
    {
        $room = Room::create(['room_name' => 'gift-gold']);
        $sender = User::factory()->create(['jjb' => 500]);
        $receiver = User::factory()->create(['jjb' => 100]);
        $outsider = User::factory()->create();

        $this->joinRoom($sender, $room);
        $this->joinRoom($receiver, $room);

        $response = $this->actingAs($sender)->postJson(route('gift.gold'), [
            'to_user' => $receiver->username,
            'room_id' => $room->id,
            'amount' => 88,
        ]);

        $response->assertOk();
        $response->assertJsonFragment([
            'status' => 'success',
            'message' => "赠送成功！已向 {$receiver->username} 赠送 88 金币。",
        ]);

        // 余额应同步完成转移，确保消息不是“通知成功但金额未变”。
        $this->assertSame(412, $sender->fresh()->jjb);
        $this->assertSame(188, $receiver->fresh()->jjb);

        $messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
        $giftMessage = collect($messages)
            ->map(fn (string $item) => json_decode($item, true))
            ->first(fn (array $item) => ($item['from_user'] ?? null) === $sender->username
                && ($item['to_user'] ?? null) === $receiver->username
                && ($item['content'] ?? null) === '赠送了你 88 金币！💝');

        $this->assertNotNull($giftMessage);
        $this->assertTrue((bool) ($giftMessage['is_secret'] ?? false));
        $this->assertSame('💰 赠金币到账', $giftMessage['toast_notification']['title'] ?? null);
        $this->assertSame('💰', $giftMessage['toast_notification']['icon'] ?? null);
        $this->assertSame('#f59e0b', $giftMessage['toast_notification']['color'] ?? null);

        // 赠送方查看房间历史时，应保留这条私聊通知。
        $senderHistory = $this->actingAs($sender)
            ->get(route('chat.room', $room->id))
            ->viewData('historyMessages');
        $this->assertTrue(collect($senderHistory)->contains(
            fn (array $item) => ($item['from_user'] ?? null) === $sender->username
                && ($item['to_user'] ?? null) === $receiver->username
                && ($item['content'] ?? null) === '赠送了你 88 金币！💝'
        ));

        // 接收方查看房间历史时，也应看到这条私聊通知。
        $receiverHistory = $this->actingAs($receiver)
            ->get(route('chat.room', $room->id))
            ->viewData('historyMessages');
        $this->assertTrue(collect($receiverHistory)->contains(
            fn (array $item) => ($item['from_user'] ?? null) === $sender->username
                && ($item['to_user'] ?? null) === $receiver->username
                && ($item['content'] ?? null) === '赠送了你 88 金币！💝'
        ));

        // 旁观者不应在历史消息里看到别人的赠金币私聊。
        $outsiderHistory = $this->actingAs($outsider)
            ->get(route('chat.room', $room->id))
            ->viewData('historyMessages');
        $this->assertFalse(collect($outsiderHistory)->contains(
            fn (array $item) => ($item['from_user'] ?? null) === $sender->username
                && ($item['to_user'] ?? null) === $receiver->username
                && ($item['content'] ?? null) === '赠送了你 88 金币！💝'
        ));
    }

    /**
     * 测试目标用户不在当前房间时不能赠送金币。
     */
    public function test_gift_gold_requires_target_online_in_same_room(): void
    {
        $room = Room::create(['room_name' => 'ggbd']);
        $sender = User::factory()->create(['jjb' => 500]);
        $receiver = User::factory()->create(['jjb' => 100]);

        $this->joinRoom($sender, $room);

        $response = $this->actingAs($sender)->postJson(route('gift.gold'), [
            'to_user' => $receiver->username,
            'room_id' => $room->id,
            'amount' => 88,
        ]);

        $response->assertStatus(403);
        $this->assertSame(500, $sender->fresh()->jjb);
        $this->assertSame(100, $receiver->fresh()->jjb);
    }

    /**
     * 测试目标用户不在当前房间时不能送花。
     */
    public function test_send_flower_requires_target_online_in_same_room(): void
    {
        $room = Room::create(['room_name' => 'flwbd']);
        $sender = User::factory()->create(['jjb' => 500]);
        $receiver = User::factory()->create(['meili' => 0]);
        $gift = Gift::query()->where('is_active', true)->firstOrFail();

        $this->joinRoom($sender, $room);

        $response = $this->actingAs($sender)->postJson(route('gift.flower'), [
            'to_user' => $receiver->username,
            'room_id' => $room->id,
            'gift_id' => $gift->id,
            'count' => 1,
        ]);

        $response->assertStatus(403);
        $this->assertSame(500, $sender->fresh()->jjb);
        $this->assertSame(0, $receiver->fresh()->meili);
    }

    /**
     * 测试会员用户首次进房时会把专属欢迎主题写入历史消息。
     */
    public function test_vip_user_join_message_uses_presence_theme_payload(): void
    {
        $room = Room::create(['room_name' => 'viproom']);
        $vipLevel = \App\Models\VipLevel::factory()->create([
            'join_effect' => 'lightning',
            'join_banner_style' => 'storm',
            'allow_custom_messages' => true,
        ]);
        $user = User::factory()->create([
            'vip_level_id' => $vipLevel->id,
            'hy_time' => now()->addDays(30),
            'custom_join_message' => '{username} 带着风暴王座闪耀降临',
            'has_received_new_gift' => true,
        ]);

        $response = $this->actingAs($user)->get(route('chat.room', $room->id));

        $response->assertStatus(200);
        $history = $response->viewData('historyMessages');
        $presenceMessage = collect($history)->first(fn (array $message) => ($message['action'] ?? '') === 'vip_presence');

        $this->assertNotNull($presenceMessage);
        $this->assertSame('join', $presenceMessage['presence_type']);
        $this->assertSame('lightning', $presenceMessage['presence_effect']);
        $this->assertSame('storm', $presenceMessage['presence_banner_style']);
        $this->assertStringContainsString($user->username, $presenceMessage['presence_text']);
    }

    /**
     * 测试可以获取所有房间的在线人数状态。
     */
    public function test_can_get_rooms_online_status()
    {
        $user = User::factory()->create();
        $room1 = Room::create(['room_name' => 'room1']);
        $room2 = Room::create(['room_name' => 'room2']);

        $this->actingAs($user)->get(route('chat.room', $room1->id));

        $response = $this->actingAs($user)->getJson(route('chat.rooms-online-status'));

        $response->assertStatus(200);

        // Assert room1 has 1 online, room2 has 0
        $response->assertJsonFragment([
            'id' => $room1->id,
            'online' => 1,
        ]);
        $response->assertJsonFragment([
            'id' => $room2->id,
            'online' => 0,
        ]);
    }

    /**
     * 测试拥有公告权限的职务用户可以设置房间公告。
     */
    public function test_position_user_can_set_announcement()
    {
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
        ]);
        $room = Room::create(['room_name' => 'test_ann', 'room_owner' => 'someone']);

        $this->joinRoom($user, $room);

        $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
            'announcement' => 'This is a new test announcement',
        ]);

        $response->assertStatus(200);

        $room->refresh();
        $this->assertStringContainsString('This is a new test announcement', $room->announcement);
    }

    /**
     * 测试拥有公告权限的职务用户可以设置房间公告。
     */
    public function test_position_user_with_room_announcement_permission_can_set_announcement(): void
    {
        $user = $this->createUserWithPositionPermissions([
            PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
        ]);
        $room = Room::create(['room_name' => 'test_ann2', 'room_owner' => 'other']);

        $this->joinRoom($user, $room);

        $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
            'announcement' => 'This is a new test announcement',
        ]);

        $response->assertStatus(200);
    }

    /**
     * 测试房主但无公告权限时也不能设置房间公告。
     */
    public function test_room_owner_without_announcement_permission_cannot_set_announcement(): void
    {
        $user = $this->createUserWithPositionPermissions([]);
        $room = Room::create(['room_name' => 'test_ann3', 'room_owner' => $user->username]);

        $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
            'announcement' => 'This is a new test announcement',
        ]);

        $response->assertStatus(403);
    }

    /**
     * 测试无权限用户不能设置房间公告。
     */
    public function test_cannot_set_announcement_without_permission()
    {
        $user = User::factory()->create(['user_level' => 0]);
        $room = Room::create(['room_name' => 'test_ann4', 'room_owner' => 'someone']);

        $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
            'announcement' => 'This is a new test announcement',
        ]);

        $response->assertStatus(403);
    }

    /**
     * 创建带指定聊天室权限的在职职务用户。
     *
     * @param  list<string>  $permissions
     */
    private function createUserWithPositionPermissions(array $permissions, array $attributes = []): User
    {
        $user = User::factory()->create(array_merge([
            'user_level' => 70,
        ], $attributes));

        $department = Department::create([
            'name' => '聊天室测试部门'.$user->id,
            'rank' => 70,
            'color' => '#1d4ed8',
            'sort_order' => 1,
            'description' => '聊天室权限测试',
        ]);

        $position = Position::create([
            'department_id' => $department->id,
            'name' => '聊天室测试职务'.$user->id,
            'icon' => '🛡️',
            'rank' => 70,
            'level' => 70,
            'sort_order' => 1,
            'permissions' => $permissions,
        ]);

        UserPosition::create([
            'user_id' => $user->id,
            'position_id' => $position->id,
            'appointed_by_user_id' => null,
            'appointed_at' => now(),
            'remark' => '聊天室权限测试',
            'is_active' => true,
        ]);

        return $user->fresh();
    }

    /**
     * 将测试用户写入指定房间在线列表，模拟已经进入聊天室的状态。
     */
    private function joinRoom(User $user, Room $room): void
    {
        Redis::hset("room:{$room->id}:users", $user->username, json_encode([
            'user_id' => $user->id,
            'username' => $user->username,
        ], JSON_UNESCAPED_UNICODE));
    }
}
