<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Mtc\Modules\Members\Classes\Auth;
use Mtc\Modules\Members\Classes\Authenticator;
use Mtc\Modules\Members\Models\Member;
use Tests\TestCase;

/**
 * Pins the hardened isLoggedIn() / getLoggedInMember() contract
 * (Authenticator.php):
 *
 *  - isLoggedIn() is a pure read: it returns false for a session that points at
 *    a member row which no longer exists, WITHOUT mutating the session. It is
 *    called from read-only contexts (Twig globals, basket) that must not have
 *    their session regenerated or basket/order cleared as a side effect.
 *  - the member-exists lookup is memoised for the request, so the many
 *    isLoggedIn() calls per page do not each issue the same query.
 *  - clearing a stale member session is done by getLoggedInMember(), where a
 *    mutation is expected.
 *
 * Requires the docker DB:
 *     make shell
 *     ./vendor/bin/phpunit --filter=MemberIsLoggedInTest
 */
class MemberIsLoggedInTest extends TestCase
{
    private const PASSWORD = 'Secret123!';

    /** @var int[] Member ids created during the test, removed in tearDown. */
    private array $createdMemberIds = [];

    protected function setUp(): void
    {
        parent::setUp();

        // The Auth facade resolves 'members_auth', which is only bound during a
        // web request (modules/Members/hooks.php, gated on MEMBERS_SECTION).
        App::bind('members_auth', fn () => new Authenticator());

        if (session_status() !== PHP_SESSION_ACTIVE) {
            @session_start();
        }

        // Isolate the request-scoped existence cache between tests.
        $this->resetMemberExistsCache();
        $_SESSION = [];
    }

    public function test_is_logged_in_true_for_an_existing_member(): void
    {
        $member = $this->createMember();

        $_SESSION['logged_in'] = true;
        $_SESSION['member_id'] = $member->id;

        $this->assertTrue(Auth::isLoggedIn());
    }

    public function test_is_logged_in_false_when_not_flagged_logged_in(): void
    {
        $member = $this->createMember();

        // member_id present but logged_in flag missing.
        $_SESSION['member_id'] = $member->id;

        $this->assertFalse(Auth::isLoggedIn());
    }

    public function test_is_logged_in_false_for_a_stale_member_without_touching_the_session(): void
    {
        $member = $this->createMember();

        $_SESSION['logged_in'] = true;
        $_SESSION['member_id'] = $member->id;
        $_SESSION['basket_id'] = 4321;
        $_SESSION['order_id']  = 9876;

        // The member row disappears (deleted / merged) while the session is live.
        $this->deleteMember($member->id);
        $this->resetMemberExistsCache();

        $this->assertFalse(Auth::isLoggedIn(), 'stale member should not be logged in');

        // The read-only isLoggedIn() must NOT have cleared any session state.
        $this->assertSame($member->id, $_SESSION['member_id'] ?? null, 'member_id must survive isLoggedIn()');
        $this->assertSame(4321, $_SESSION['basket_id'] ?? null, 'basket_id must survive isLoggedIn()');
        $this->assertSame(9876, $_SESSION['order_id'] ?? null, 'order_id must survive isLoggedIn()');
    }

    public function test_member_exists_check_is_memoised_for_the_request(): void
    {
        $member = $this->createMember();

        $_SESSION['logged_in'] = true;
        $_SESSION['member_id'] = $member->id;

        // First call primes the cache with a positive result.
        $this->assertTrue(Auth::isLoggedIn());

        // Row is deleted, but WITHOUT resetting the cache the memoised result stands.
        $this->deleteMember($member->id);

        $this->assertTrue(Auth::isLoggedIn(), 'memoised existence result should be reused within the request');
    }

    public function test_get_logged_in_member_clears_a_stale_session(): void
    {
        $member = $this->createMember();

        $_SESSION['logged_in'] = true;
        $_SESSION['member_id'] = $member->id;
        $_SESSION['basket_id'] = 4321;

        $this->deleteMember($member->id);
        $this->resetMemberExistsCache();

        $resolved = Auth::getLoggedInMember();

        $this->assertFalse($resolved->exists, 'a stale member resolves to a non-existent Member');
        $this->assertArrayNotHasKey('member_id', $_SESSION, 'getLoggedInMember() clears the stale member session');
        $this->assertArrayNotHasKey('basket_id', $_SESSION, 'getLoggedInMember() clears member basket state');
    }

    private function createMember(): Member
    {
        $member = new Member();
        $member->email              = 'isloggedin+' . uniqid() . '@example.com';
        $member->password           = Auth::hashPassword(self::PASSWORD);
        $member->first_login        = date('Y-m-d H:i:s');
        $member->failed_login_count = 0;
        $member->save();

        $this->createdMemberIds[] = $member->id;

        return $member;
    }

    private function deleteMember($id): void
    {
        DB::table('members')->where('id', $id)->delete();
        $this->createdMemberIds = array_diff($this->createdMemberIds, [$id]);
    }

    /**
     * Clears Authenticator's static request-scoped existence cache so each test
     * starts from a clean state (mirrors a fresh request).
     */
    private function resetMemberExistsCache(): void
    {
        $reflection = new \ReflectionProperty(Authenticator::class, 'memberExistsCache');
        $reflection->setAccessible(true);
        $reflection->setValue(null, []);
    }

    protected function tearDown(): void
    {
        if (!empty($this->createdMemberIds)) {
            DB::table('members')->whereIn('id', $this->createdMemberIds)->delete();
        }

        $_SESSION = [];

        parent::tearDown();
    }
}
