<?php

namespace Tests\Feature\Security\Presence;

use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use App\Services\PresenceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Spatie\Activitylog\Models\Activity;
use Tests\TestCase;

/**
 * Presence System Security Tests
 *
 * Tests security aspects of the presence system to ensure:
 * - Users cannot spoof presence of other users
 * - Guard separation is enforced
 * - No IDOR vulnerabilities in presence updates
 * - No sensitive data exposed through presence system
 * - Cache poisoning prevention
 *
 * @group security
 * @group presence
 * @group critical
 */
class PresenceSystemSecurityTest extends TestCase
{
    use RefreshDatabase;

    protected PresenceService $presenceService;

    protected function setUp(): void
    {
        parent::setUp();
        $this->presenceService = app(PresenceService::class);
    }

    // ===========================================
    // IDOR PREVENTION TESTS
    // ===========================================

    /** @test */
    public function user_cannot_update_presence_for_another_user()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        // Authenticate as user1
        $this->actingAs($user1, 'web');

        // Update presence (should only affect authenticated user)
        $this->presenceService->updatePresence();

        // Verify only user1's presence was updated, not user2's
        $this->assertTrue($this->presenceService->isUserOnline($user1, 'web'));

        // Check activities to verify user2 has no presence logged
        $user2Activities = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user2->id)
            ->where('subject_type', get_class($user2))
            ->count();

        $this->assertEquals(0, $user2Activities, 'User2 should have no presence activities');
    }

    /** @test */
    public function presence_update_accepts_null_and_uses_authenticated_user()
    {
        $user = User::factory()->create();

        $this->actingAs($user, 'web');

        // Call without parameters - should use Auth::user()
        $this->presenceService->updatePresence();

        // Verify the authenticated user's presence was updated
        $latestActivity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->latest()
            ->first();

        $this->assertNotNull($latestActivity, 'Authenticated user presence should be logged');
        $this->assertEquals($user->id, $latestActivity->subject_id);
    }

    /** @test */
    public function unauthenticated_user_cannot_update_presence()
    {
        // No authentication
        $this->presenceService->updatePresence();

        // Verify no presence activities were created
        $activityCount = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->count();

        $this->assertEquals(0, $activityCount, 'Unauthenticated users should not create presence records');
    }

    // ===========================================
    // GUARD SEPARATION TESTS
    // ===========================================

    /** @test */
    public function presence_is_guard_specific()
    {
        $user = User::factory()->create();

        // Set user online on web guard
        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Verify user is online on web guard
        $this->assertTrue($this->presenceService->isUserOnline($user, 'web'));

        // Check activity logs directly to verify guard separation
        $webActivity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->where('properties->guard', 'web')
            ->exists();

        $adminActivity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->where('properties->guard', 'admin')
            ->exists();

        $this->assertTrue($webActivity, 'Should have activity on web guard');
        $this->assertFalse($adminActivity, 'Should NOT have activity on admin guard');

        // Now log presence on a different guard for the same user
        $this->presenceService->updatePresence($user, 'admin');

        // Now both guards should have activities
        $adminActivity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->where('properties->guard', 'admin')
            ->exists();

        $this->assertTrue($adminActivity, 'Should now have activity on admin guard');
    }

    /** @test */
    public function online_users_list_is_guard_specific()
    {
        $user = User::factory()->create();
        $organization = Organization::factory()->create();
        $bank = Bank::factory()->create();

        // Set different profiles online on different guards
        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        $this->actingAs($organization, 'organization');
        $this->presenceService->updatePresence($organization, 'organization');

        $this->actingAs($bank, 'bank');
        $this->presenceService->updatePresence($bank, 'bank');

        // Get online users per guard
        $webOnlineUsers = $this->presenceService->getOnlineUsers('web');
        $orgOnlineUsers = $this->presenceService->getOnlineUsers('organization');
        $bankOnlineUsers = $this->presenceService->getOnlineUsers('bank');

        // Verify guard separation
        $this->assertEquals(1, $webOnlineUsers->count());
        $this->assertEquals($user->id, $webOnlineUsers->first()['id']);

        $this->assertEquals(1, $orgOnlineUsers->count());
        $this->assertEquals($organization->id, $orgOnlineUsers->first()['id']);

        $this->assertEquals(1, $bankOnlineUsers->count());
        $this->assertEquals($bank->id, $bankOnlineUsers->first()['id']);
    }

    /** @test */
    public function cannot_spoof_guard_in_presence_update()
    {
        $user = User::factory()->create();

        // Authenticate as web user
        $this->actingAs($user, 'web');

        // Try to update presence with wrong guard
        $this->presenceService->updatePresence($user, 'admin');

        // Verify the activity was still logged with the specified guard
        // (This is by design - the function trusts the passed guard parameter)
        // But the key security is that Auth::user() is always used, not a passed user
        $activity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->latest()
            ->first();

        $this->assertNotNull($activity);

        // The guard parameter is logged as-is, but this is not a security issue
        // because the authenticated user (from Auth) is what matters
        $properties = $activity->properties;
        $this->assertEquals('admin', $properties['guard']);

        // The important security check: the subject is still the authenticated web user
        $this->assertEquals(get_class($user), $activity->subject_type);
        $this->assertEquals($user->id, $activity->subject_id);
    }

    // ===========================================
    // CACHE POISONING PREVENTION
    // ===========================================

    /** @test */
    public function cache_keys_are_guard_specific_preventing_poisoning()
    {
        $user = User::factory()->create();

        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Get cache keys
        $webCacheKey = "presence_web_{$user->id}";
        $adminCacheKey = "presence_admin_{$user->id}";

        // Verify web cache exists
        $webCache = Cache::get($webCacheKey);
        $this->assertNotNull($webCache);
        $this->assertEquals('web', $webCache['guard']);

        // Update on different guard
        $this->presenceService->updatePresence($user, 'admin');

        // Verify admin cache now exists with correct guard
        $adminCache = Cache::get($adminCacheKey);
        $this->assertNotNull($adminCache);
        $this->assertEquals('admin', $adminCache['guard']);

        // Verify they are separate cache entries
        $this->assertNotEquals($webCache['guard'], $adminCache['guard']);
    }

    /** @test */
    public function offline_status_clears_cache()
    {
        $user = User::factory()->create();

        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Verify cache exists
        $cacheKey = "presence_web_{$user->id}";
        $this->assertNotNull(Cache::get($cacheKey));

        // Set user offline
        $this->presenceService->setUserOffline($user, 'web');

        // Verify cache was cleared
        $this->assertNull(Cache::get($cacheKey));
    }

    /** @test */
    public function online_users_cache_has_reasonable_ttl()
    {
        $user = User::factory()->create();

        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Get online users (this caches the result)
        $onlineUsers = $this->presenceService->getOnlineUsers('web');

        // Verify cache key exists
        $cacheKey = "online_users_web_" . PresenceService::ONLINE_THRESHOLD_MINUTES;
        $this->assertNotNull(Cache::get($cacheKey));

        // Cache TTL is 30 seconds (defined in the service)
        // This is reasonable to prevent stale data while avoiding excessive queries
        $this->assertEquals(1, $onlineUsers->count());
    }

    // ===========================================
    // DATA EXPOSURE PREVENTION
    // ===========================================

    /** @test */
    public function presence_data_does_not_expose_sensitive_information()
    {
        $user = User::factory()->create([
            'email' => 'sensitive@example.com',
            'password' => bcrypt('secret123'),
        ]);

        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Get online users data
        $onlineUsers = $this->presenceService->getOnlineUsers('web');
        $userData = $onlineUsers->first();

        // Verify only safe data is exposed
        $this->assertArrayHasKey('id', $userData);
        $this->assertArrayHasKey('name', $userData);
        $this->assertArrayHasKey('avatar', $userData);
        $this->assertArrayHasKey('guard', $userData);
        $this->assertArrayHasKey('last_seen', $userData);
        $this->assertArrayHasKey('status', $userData);

        // Verify sensitive data is NOT exposed
        $this->assertArrayNotHasKey('email', $userData);
        $this->assertArrayNotHasKey('password', $userData);
        $this->assertArrayNotHasKey('remember_token', $userData);
    }

    /** @test */
    public function presence_cache_does_not_expose_sensitive_information()
    {
        $user = User::factory()->create([
            'email' => 'sensitive@example.com',
            'password' => bcrypt('secret123'),
        ]);

        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Get cached data
        $cacheKey = "presence_web_{$user->id}";
        $cachedData = Cache::get($cacheKey);

        // Verify only safe data is cached
        $this->assertArrayHasKey('user_id', $cachedData);
        $this->assertArrayHasKey('user_type', $cachedData);
        $this->assertArrayHasKey('guard', $cachedData);
        $this->assertArrayHasKey('name', $cachedData);
        $this->assertArrayHasKey('avatar', $cachedData);
        $this->assertArrayHasKey('last_seen', $cachedData);
        $this->assertArrayHasKey('status', $cachedData);

        // Verify sensitive data is NOT cached
        $this->assertArrayNotHasKey('email', $cachedData);
        $this->assertArrayNotHasKey('password', $cachedData);
    }

    /** @test */
    public function presence_activity_log_does_not_expose_passwords()
    {
        $user = User::factory()->create([
            'password' => bcrypt('secret123'),
        ]);

        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Get activity log
        $activity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->latest()
            ->first();

        // Verify properties don't contain sensitive data
        $properties = $activity->properties;
        $this->assertArrayNotHasKey('password', $properties->toArray());
        $this->assertArrayNotHasKey('email', $properties->toArray());

        // Verify only metadata is logged
        $this->assertArrayHasKey('guard', $properties);
        $this->assertArrayHasKey('status', $properties);
        $this->assertArrayHasKey('ip_address', $properties);
        $this->assertArrayHasKey('user_agent', $properties);
    }

    // ===========================================
    // MULTI-GUARD PROFILE TESTS
    // ===========================================

    /** @test */
    public function admin_presence_is_tracked_separately_from_user()
    {
        $admin = Admin::factory()->create();
        $user = User::factory()->create();

        // Set admin online on admin guard
        $this->actingAs($admin, 'admin');
        $this->presenceService->updatePresence($admin, 'admin');

        // Set user online on web guard
        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Verify each is tracked on their own guard
        $this->assertTrue($this->presenceService->isUserOnline($admin, 'admin'));
        $this->assertTrue($this->presenceService->isUserOnline($user, 'web'));

        // Verify they are different models (Admin vs User)
        $adminActivity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_type', get_class($admin))
            ->where('subject_id', $admin->id)
            ->where('properties->guard', 'admin')
            ->latest()
            ->first();

        $userActivity = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_type', get_class($user))
            ->where('subject_id', $user->id)
            ->where('properties->guard', 'web')
            ->latest()
            ->first();

        $this->assertNotNull($adminActivity);
        $this->assertNotNull($userActivity);
        $this->assertNotEquals($adminActivity->subject_type, $userActivity->subject_type);
    }

    /** @test */
    public function bank_presence_respects_guard_boundaries()
    {
        $centralBank = Bank::factory()->create(['level' => 0]);
        $regularBank = Bank::factory()->create(['level' => 1]);

        // Set both banks online
        $this->actingAs($centralBank, 'bank');
        $this->presenceService->updatePresence($centralBank, 'bank');

        $this->actingAs($regularBank, 'bank');
        $this->presenceService->updatePresence($regularBank, 'bank');

        // Get online banks
        $onlineBanks = $this->presenceService->getOnlineUsers('bank');

        // Verify both banks are tracked
        $this->assertEquals(2, $onlineBanks->count());

        // Verify banks don't appear in web guard
        $onlineWebUsers = $this->presenceService->getOnlineUsers('web');
        $this->assertEquals(0, $onlineWebUsers->count());
    }

    /** @test */
    public function organization_presence_is_independent_from_users()
    {
        $user = User::factory()->create();
        $organization = Organization::factory()->create();
        $organization->users()->attach($user->id);

        // Set user online on web guard
        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Set organization online (different guard, even though same underlying user)
        $this->actingAs($organization, 'organization');
        $this->presenceService->updatePresence($organization, 'organization');

        // Verify independent tracking
        $webOnline = $this->presenceService->getOnlineUsers('web');
        $orgOnline = $this->presenceService->getOnlineUsers('organization');

        $this->assertEquals(1, $webOnline->count());
        $this->assertEquals(1, $orgOnline->count());

        // Verify they are different entities
        $this->assertEquals($user->id, $webOnline->first()['id']);
        $this->assertEquals($organization->id, $orgOnline->first()['id']);
        $this->assertNotEquals($webOnline->first()['user_type'], $orgOnline->first()['user_type']);
    }

    // ===========================================
    // LIVEWIRE COMPONENT SECURITY
    // ===========================================

    /** @test */
    public function profile_status_badge_cannot_be_exploited_for_idor()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        // Set both users online
        $this->actingAs($user1, 'web');
        $this->presenceService->updatePresence($user1, 'web');

        $this->actingAs($user2, 'web');
        $this->presenceService->updatePresence($user2, 'web');

        // User1 can check user2's status (presence is intentionally public for time banking)
        $isUser2Online = $this->presenceService->isUserOnline($user2, 'web');

        // This should return true since user2 is online
        $this->assertTrue($isUser2Online);

        // But this is NOT an IDOR vulnerability because:
        // 1. Presence is read-only (cannot manipulate another user's status)
        // 2. No sensitive data is exposed (only id, name, avatar, last_seen)
        // 3. This is intentional design for a time banking platform

        // The key security principle: users can only affect their own presence
        // Even though isUserOnline() allows checking any user's status (intentionally public),
        // users cannot manipulate other users' presence status

        // Verify the presence data doesn't expose manipulation capabilities
        $onlineUsers = $this->presenceService->getOnlineUsers('web');
        $this->assertEquals(2, $onlineUsers->count());

        // Verify both users are listed
        $userIds = $onlineUsers->pluck('id')->toArray();
        $this->assertContains($user1->id, $userIds);
        $this->assertContains($user2->id, $userIds);
    }

    /** @test */
    public function profile_status_badge_does_not_allow_status_manipulation()
    {
        $user = User::factory()->create();
        $attacker = User::factory()->create();

        // Set user online
        $this->actingAs($user, 'web');
        $this->presenceService->updatePresence($user, 'web');

        // Authenticate as attacker
        $this->actingAs($attacker, 'web');

        // Attacker tries to set user offline
        // (The service only allows setting authenticated user offline)
        $this->presenceService->setUserOffline($attacker, 'web');

        // Verify user is still online (attacker only affected themselves)
        $this->assertTrue($this->presenceService->isUserOnline($user, 'web'));
        $this->assertFalse($this->presenceService->isUserOnline($attacker, 'web'));
    }

    // ===========================================
    // CLEANUP AND MAINTENANCE
    // ===========================================

    /** @test */
    public function presence_cleanup_prevents_database_bloat()
    {
        $user = User::factory()->create();
        $this->actingAs($user, 'web');

        // Create multiple presence updates
        for ($i = 0; $i < 10; $i++) {
            $this->presenceService->updatePresence($user, 'web');
        }

        // Check how many records exist
        $recordCount = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('causer_id', $user->id)
            ->count();

        // Should be limited by keep_last_presence_updates config
        $keepCount = timebank_config('presence_settings.keep_last_presence_updates', 5);
        $this->assertLessThanOrEqual($keepCount, $recordCount);
    }

    /** @test */
    public function offline_status_is_logged_as_activity()
    {
        $user = User::factory()->create();
        $this->actingAs($user, 'web');

        // Set online
        $this->presenceService->updatePresence($user, 'web');

        $onlineActivityCount = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->count();

        $this->assertEquals(1, $onlineActivityCount, 'Should have 1 online activity');

        // Set offline
        $this->presenceService->setUserOffline($user, 'web');

        // Check activity count increased (offline logged)
        $totalActivityCount = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->count();

        $this->assertEquals(2, $totalActivityCount, 'Should have 2 activities (online + offline)');

        // Verify offline status is logged in activity log (even if not used for current online check)
        $activities = Activity::where('log_name', PresenceService::PRESENCE_ACTIVITY)
            ->where('subject_id', $user->id)
            ->where('subject_type', get_class($user))
            ->orderBy('id', 'desc')
            ->get();

        // Should have both online and offline activities
        $this->assertCount(2, $activities);

        // Check both activities exist
        $statuses = $activities->pluck('properties')->pluck('status')->toArray();
        $this->assertContains('online', $statuses);
        $this->assertContains('offline', $statuses);
    }
}
