<?php

/**
 * 文件功能：每日签到前台接口测试
 *
 * 覆盖签到领取、重复拦截、连续天数、身份有效期与聊天室播报。
 */

namespace Tests\Feature;

use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Events\UserStatusUpdated;
use App\Models\DailySignIn;
use App\Models\Room;
use App\Models\ShopItem;
use App\Models\SignInRewardRule;
use App\Models\User;
use App\Models\UserPurchase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;

/**
 * 类功能：验证每日签到前台接口的核心行为。
 */
class DailySignInControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * 方法功能：每个用例前清理 Redis 与时间，避免跨测试污染。
     */
    protected function setUp(): void
    {
        parent::setUp();
        Redis::flushall();
        Carbon::setTestNow('2026-04-24 10:00:00');
    }

    /**
     * 方法功能：每个用例后恢复系统时间。
     */
    protected function tearDown(): void
    {
        Carbon::setTestNow();
        parent::tearDown();
    }

    /**
     * 测试首次签到会发放奖励、写入流水并生成身份。
     */
    public function test_user_can_claim_daily_sign_in_reward(): void
    {
        $user = User::factory()->create(['jjb' => 10, 'exp_num' => 0, 'meili' => 0]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], [
            'streak_days' => 1,
            'gold_reward' => 100,
            'exp_reward' => 20,
            'charm_reward' => 3,
            'identity_badge_code' => 'sign_1',
            'identity_badge_name' => '签到新星',
            'identity_badge_icon' => '⭐',
            'identity_badge_color' => '#0f766e',
            'identity_duration_days' => 7,
        ]);

        $response = $this->actingAs($user)->postJson(route('daily-sign-in.claim'));

        $response->assertOk()
            ->assertJsonPath('status', 'success')
            ->assertJsonPath('data.sign_in.streak_days', 1)
            ->assertJsonPath('data.user.jjb', 110)
            ->assertJsonPath('data.identity.label', '签到新星');

        $this->assertDatabaseHas('daily_sign_ins', [
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-24',
            'streak_days' => 1,
            'gold_reward' => 100,
            'exp_reward' => 20,
            'charm_reward' => 3,
        ]);
        $this->assertDatabaseHas('user_currency_logs', [
            'user_id' => $user->id,
            'currency' => 'gold',
            'amount' => 100,
            'source' => CurrencySource::SIGN_IN->value,
        ]);
        $this->assertDatabaseHas('user_identity_badges', [
            'user_id' => $user->id,
            'source' => 'sign_in',
            'badge_code' => 'sign_1',
            'is_active' => true,
        ]);
    }

    /**
     * 测试同一天重复签到不会重复发放奖励。
     */
    public function test_duplicate_daily_sign_in_is_rejected_without_second_reward(): void
    {
        $user = User::factory()->create(['jjb' => 0]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], [
            'streak_days' => 1,
            'gold_reward' => 50,
        ]);

        $this->actingAs($user)->postJson(route('daily-sign-in.claim'))->assertOk();
        $currencyLogCount = \App\Models\UserCurrencyLog::query()
            ->where('user_id', $user->id)
            ->where('source', CurrencySource::SIGN_IN->value)
            ->count();

        $response = $this->actingAs($user)->postJson(route('daily-sign-in.claim'));

        $response->assertStatus(422)
            ->assertJsonPath('status', 'error')
            ->assertJsonPath('message', '今日已签到，请明天再来。');

        $this->assertSame(1, DailySignIn::query()->where('user_id', $user->id)->count());
        $this->assertSame($currencyLogCount, \App\Models\UserCurrencyLog::query()->where('user_id', $user->id)->where('source', CurrencySource::SIGN_IN->value)->count());
        $this->assertSame(50, (int) $user->fresh()->jjb);
    }

    /**
     * 测试连续签到会递增，断签后会重新从 1 开始。
     */
    public function test_streak_increments_and_resets_after_gap(): void
    {
        $user = User::factory()->create();
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], ['streak_days' => 1, 'gold_reward' => 1]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 2], ['streak_days' => 2, 'gold_reward' => 2]);

        $this->actingAs($user)->postJson(route('daily-sign-in.claim'))->assertJsonPath('data.sign_in.streak_days', 1);

        Carbon::setTestNow('2026-04-25 10:00:00');
        $this->actingAs($user)->postJson(route('daily-sign-in.claim'))->assertJsonPath('data.sign_in.streak_days', 2);

        Carbon::setTestNow('2026-04-27 10:00:00');
        $this->actingAs($user)->postJson(route('daily-sign-in.claim'))->assertJsonPath('data.sign_in.streak_days', 1);
    }

    /**
     * 测试连续签到满一年后下一天会重新从 1 计算。
     */
    public function test_streak_resets_after_yearly_cycle_is_completed(): void
    {
        $user = User::factory()->create();
        DailySignIn::factory()->create([
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-23',
            'streak_days' => 365,
        ]);

        $this->actingAs($user)
            ->postJson(route('daily-sign-in.claim'))
            ->assertOk()
            ->assertJsonPath('data.sign_in.streak_days', 1);
    }

    /**
     * 测试闰年需要满 366 天后才重新计算。
     */
    public function test_leap_year_streak_resets_after_366_days(): void
    {
        $user = User::factory()->create();
        Carbon::setTestNow('2024-12-31 10:00:00');
        DailySignIn::factory()->create([
            'user_id' => $user->id,
            'sign_in_date' => '2024-12-30',
            'streak_days' => 365,
        ]);

        $this->actingAs($user)
            ->postJson(route('daily-sign-in.claim'))
            ->assertOk()
            ->assertJsonPath('data.sign_in.streak_days', 366);

        Carbon::setTestNow('2025-01-01 10:00:00');
        $this->actingAs($user)
            ->postJson(route('daily-sign-in.claim'))
            ->assertOk()
            ->assertJsonPath('data.sign_in.streak_days', 1);
    }

    /**
     * 测试身份有效期过后不会继续出现在状态接口。
     */
    public function test_expired_identity_is_not_returned_in_status(): void
    {
        $user = User::factory()->create();
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], [
            'streak_days' => 1,
            'identity_badge_code' => 'short',
            'identity_badge_name' => '短期身份',
            'identity_badge_icon' => '⏳',
            'identity_duration_days' => 1,
        ]);

        $this->actingAs($user)->postJson(route('daily-sign-in.claim'))->assertOk();

        Carbon::setTestNow('2026-04-26 10:00:00');
        $this->actingAs($user)
            ->getJson(route('daily-sign-in.status'))
            ->assertOk()
            ->assertJsonPath('data.identity', null);
    }

    /**
     * 测试当前房间签到会写入聊天室通知并附带快速签到按钮。
     */
    public function test_claim_with_room_broadcasts_chat_notice_with_quick_button(): void
    {
        Event::fake([MessageSent::class, UserStatusUpdated::class]);

        $room = Room::create(['room_name' => 'testroom', 'door_open' => true]);
        $user = User::factory()->create(['jjb' => 0]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], [
            'streak_days' => 1,
            'gold_reward' => 100,
            'exp_reward' => 20,
        ]);

        Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE));
        Redis::setex("room:{$room->id}:alive:{$user->username}", 90, 1);

        $response = $this->actingAs($user)->postJson(route('daily-sign-in.claim'), [
            'room_id' => $room->id,
        ]);

        $response->assertOk();

        $rawMessage = Redis::lindex("room:{$room->id}:messages", -1);
        $message = json_decode((string) $rawMessage, true);
        $this->assertSame('签到播报', $message['from_user']);
        $this->assertStringContainsString('快速签到', $message['content']);
        $this->assertStringContainsString('quickDailySignIn', $message['content']);

        Event::assertDispatched(MessageSent::class, function (MessageSent $event) use ($room): bool {
            return $event->roomId === $room->id
                && ($event->message['from_user'] ?? null) === '签到播报';
        });
    }

    /**
     * 测试不携带房间 ID 时不会发送聊天室通知。
     */
    public function test_claim_without_room_does_not_write_chat_notice(): void
    {
        Event::fake([MessageSent::class]);

        $user = User::factory()->create();
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], ['streak_days' => 1, 'gold_reward' => 1]);

        $this->actingAs($user)->postJson(route('daily-sign-in.claim'))->assertOk();

        $this->assertSame([], Redis::keys('room:*:messages'));
        Event::assertNotDispatched(MessageSent::class);
    }

    /**
     * 测试签到日历会展示已签、漏签和补签卡数量。
     */
    public function test_calendar_returns_month_days_and_makeup_card_count(): void
    {
        $user = User::factory()->create();
        $card = ShopItem::query()->where('slug', 'sign_repair_card')->firstOrFail();
        UserPurchase::query()->create([
            'user_id' => $user->id,
            'shop_item_id' => $card->id,
            'status' => 'active',
            'price_paid' => 1200,
        ]);
        DailySignIn::factory()->create([
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-23',
            'streak_days' => 1,
        ]);

        $response = $this->actingAs($user)->getJson(route('daily-sign-in.calendar', ['month' => '2026-04']));

        $response->assertOk()
            ->assertJsonPath('data.month', '2026-04')
            ->assertJsonPath('data.makeup_card_count', 1)
            ->assertJsonPath('data.sign_repair_card_item.slug', 'sign_repair_card')
            ->assertJsonPath('data.sign_repair_card_item.price', 10000)
            ->assertJsonPath('data.reward_rules.0.streak_days', 1);

        $days = collect($response->json('data.days'));
        $this->assertTrue((bool) $days->firstWhere('date', '2026-04-23')['signed']);
        $this->assertTrue((bool) $days->firstWhere('date', '2026-04-22')['can_makeup']);

        $previousMonthResponse = $this->actingAs($user)->getJson(route('daily-sign-in.calendar', ['month' => '2026-03']));
        $previousMonthDays = collect($previousMonthResponse->json('data.days'));
        $this->assertFalse((bool) $previousMonthDays->firstWhere('date', '2026-03-31')['can_makeup']);
    }

    /**
     * 测试用户可以消耗补签卡补签历史漏签日期。
     */
    public function test_user_can_makeup_missed_day_with_sign_repair_card(): void
    {
        Event::fake([MessageSent::class, UserStatusUpdated::class]);

        $user = User::factory()->create(['jjb' => 0]);
        $room = Room::create(['room_name' => '补签通知房', 'door_open' => true]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 1], [
            'streak_days' => 1,
            'gold_reward' => 10,
        ]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 2], [
            'streak_days' => 2,
            'gold_reward' => 20,
        ]);
        SignInRewardRule::query()->updateOrCreate(['streak_days' => 3], [
            'streak_days' => 3,
            'gold_reward' => 30,
        ]);
        $card = ShopItem::query()->where('slug', 'sign_repair_card')->firstOrFail();
        $purchase = UserPurchase::query()->create([
            'user_id' => $user->id,
            'shop_item_id' => $card->id,
            'status' => 'active',
            'price_paid' => 1200,
        ]);
        DailySignIn::factory()->create([
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-22',
            'streak_days' => 1,
        ]);
        DailySignIn::factory()->create([
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-24',
            'streak_days' => 1,
        ]);
        Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE));
        Redis::setex("room:{$room->id}:alive:{$user->username}", 90, 1);

        $response = $this->actingAs($user)->postJson(route('daily-sign-in.makeup'), [
            'target_date' => '2026-04-23',
            'room_id' => $room->id,
        ]);

        $response->assertOk()
            ->assertJsonPath('status', 'success')
            ->assertJsonPath('data.sign_in.is_makeup', true)
            ->assertJsonPath('data.sign_in.streak_days', 2)
            ->assertJsonPath('data.current_streak_days', 3);

        $this->assertStringContainsString('当前连续签到 3 天', (string) $response->json('message'));

        $this->assertDatabaseHas('daily_sign_ins', [
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-23',
            'is_makeup' => true,
            'makeup_purchase_id' => $purchase->id,
            'streak_days' => 2,
            'gold_reward' => 30,
        ]);
        $this->assertDatabaseHas('daily_sign_ins', [
            'user_id' => $user->id,
            'sign_in_date' => '2026-04-24',
            'streak_days' => 3,
        ]);
        $this->assertSame('used', $purchase->fresh()->status);
        $this->assertSame(30, (int) $user->fresh()->jjb);
        $this->assertDatabaseHas('user_currency_logs', [
            'user_id' => $user->id,
            'currency' => 'gold',
            'amount' => 30,
            'source' => CurrencySource::SIGN_IN->value,
            'remark' => '补签 2026-04-23：当前连续签到 3 天',
        ]);

        $rawMessage = Redis::lindex("room:{$room->id}:messages", -1);
        $message = json_decode((string) $rawMessage, true);
        $this->assertSame('签到播报', $message['from_user']);
        $this->assertStringContainsString('使用补签卡补签 2026-04-23', $message['content']);
        $this->assertStringContainsString('当前连续签到 3 天', $message['content']);

        Event::assertDispatched(MessageSent::class, function (MessageSent $event) use ($room): bool {
            return $event->roomId === $room->id
                && str_contains((string) ($event->message['content'] ?? ''), '使用补签卡补签 2026-04-23');
        });
    }

    /**
     * 测试没有补签卡时不能补签。
     */
    public function test_makeup_requires_available_sign_repair_card(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->postJson(route('daily-sign-in.makeup'), [
            'target_date' => '2026-04-23',
        ]);

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

    /**
     * 测试补签卡不能补签本月之外的漏签日期。
     */
    public function test_makeup_only_allows_current_month_missed_days(): void
    {
        $user = User::factory()->create();
        $card = ShopItem::query()->where('slug', 'sign_repair_card')->firstOrFail();
        $purchase = UserPurchase::query()->create([
            'user_id' => $user->id,
            'shop_item_id' => $card->id,
            'status' => 'active',
            'price_paid' => 1200,
        ]);

        $response = $this->actingAs($user)->postJson(route('daily-sign-in.makeup'), [
            'target_date' => '2026-03-31',
        ]);

        $response->assertStatus(422)
            ->assertJsonValidationErrors('target_date')
            ->assertJsonPath('errors.target_date.0', '补签卡只能补签本月的未签到日期。');

        $this->assertSame('active', $purchase->fresh()->status);
        $this->assertDatabaseMissing('daily_sign_ins', [
            'user_id' => $user->id,
            'sign_in_date' => '2026-03-31',
        ]);
    }
}
