<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * See the LICENSE file in the project root for the full license text.
 */

declare(strict_types=1);

namespace App\Tests\Repositories;

use App\Repositories\UserRepository;
use App\Tests\TestCase;

/**
 * Covers the upsert + admin-promotion contract used by the auth flows.
 * Caller-side gating (R01-N03's BOOTSTRAP_ADMIN_* env-bootstrap) lives in
 * `AuthController` + `BootstrapAdmin`; this suite only pins the repo's
 * mechanical promoteToAdmin / forceAdmin behaviour.
 */
final class UserRepositoryTest extends TestCase
{
    public function testFirstUserBecomesAdminWhenPromoted(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $this->assertSame(0, $users->count());

        $r = $users->upsertFromOidc(
            oid:            'oid-alice',
            email:          'alice@example.com',
            name:           'Alice',
            promoteToAdmin: true, // caller's decision — see BootstrapAdmin::matches
        );

        $this->assertTrue($r['user']->isAdmin);
        $this->assertNull($r['before']);
    }

    public function testSecondUserDoesNotBecomeAdmin(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-alice', 'alice@x', 'Alice', true);
        // Caller passes promoteToAdmin=false because an admin already exists.
        $r2 = $users->upsertFromOidc('oid-bob', 'bob@x', 'Bob', false);

        $this->assertFalse($r2['user']->isAdmin);
    }

    public function testReLoginDoesNotRegressAdminStatus(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-alice', 'alice@x', 'Alice', true);
        // simulate a later login: count > 0 so promoteToAdmin=false
        $r = $users->upsertFromOidc('oid-alice', 'alice@x', 'Alice', false);

        $this->assertTrue($r['user']->isAdmin, 're-login must not demote admin');
        $this->assertNotNull($r['before']);
    }

    public function testForceAdminPromotesEvenOnUpdate(): void
    {
        // Local-admin login path sets forceAdmin=true so a demoted user gets
        // promoted back on next sign-in.
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $r1 = $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', true, true);
        $this->assertTrue($r1['user']->isAdmin);

        // Manually demote.
        $pdo->exec('UPDATE users SET is_admin = 0 WHERE id = ' . $r1['user']->id);

        $r2 = $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', false, true);
        $this->assertTrue($r2['user']->isAdmin, 'forceAdmin must re-promote on update');
    }

    public function testUpsertUpdatesEmailAndName(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-alice', 'old@x', 'Old Name', true);
        $r = $users->upsertFromOidc('oid-alice', 'new@x', 'New Name', false);

        $this->assertSame('new@x',    $r['user']->email);
        $this->assertSame('New Name', $r['user']->displayName);
    }

    public function testCountReflectsInsertedUsers(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $this->assertSame(0, $users->count());
        $users->upsertFromOidc('oid-1', 'a@x', 'A', true);
        $this->assertSame(1, $users->count());
        $users->upsertFromOidc('oid-2', 'b@x', 'B', false);
        $this->assertSame(2, $users->count());
        // Re-upsert existing user shouldn't add a row.
        $users->upsertFromOidc('oid-1', 'a@x', 'A', false);
        $this->assertSame(2, $users->count());
    }

    // ------------------------------------------------------------------
    // Phase 9: users management page helpers
    // ------------------------------------------------------------------

    public function testAllReturnsEveryUserOrderedByEmail(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-c', 'carol@x',  'Carol', true);
        $users->upsertFromOidc('oid-a', 'alice@x',  'Alice', false);
        $users->upsertFromOidc('oid-b', 'BOB@x',    'Bob',   false);

        $all = $users->all();
        $this->assertCount(3, $all);
        $this->assertSame(['alice@x', 'BOB@x', 'carol@x'], array_map(fn($u) => $u->email, $all));
    }

    public function testCountAdmins(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $this->assertSame(0, $users->countAdmins());
        $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
        $this->assertSame(1, $users->countAdmins());
        $users->upsertFromOidc('oid-b', 'b@x', 'B', false);
        $this->assertSame(1, $users->countAdmins());
        $users->upsertFromOidc('oid-c', 'c@x', 'C', false, true); // forceAdmin
        $this->assertSame(2, $users->countAdmins());
    }

    public function testSetAdminTogglesAndReportsDiff(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
        $users->upsertFromOidc('oid-b', 'b@x', 'B', false);

        $bob = $users->findByOid('oid-b');
        $r = $users->setAdmin($bob->id, true);
        $this->assertFalse($r['before']->isAdmin);
        $this->assertTrue ($r['after']->isAdmin);
        $this->assertSame(2, $users->countAdmins());

        $r = $users->setAdmin($bob->id, false);
        $this->assertTrue ($r['before']->isAdmin);
        $this->assertFalse($r['after']->isAdmin);
        $this->assertSame(1, $users->countAdmins());
    }

    // ------------------------------------------------------------------
    // R01-N23: tombstone (soft-delete for privacy)
    // ------------------------------------------------------------------

    public function testFreshUserIsNotTombstoned(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $r = $users->upsertFromOidc('oid-a', 'a@x', 'Alice', true);
        $this->assertFalse($r['user']->isTombstoned());
        $this->assertNull($r['user']->tombstonedAt);
    }

    public function testSetTombstonedStampsAndForcesDemote(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-a', 'a@x', 'A', true); // first admin
        $users->upsertFromOidc('oid-b', 'b@x', 'B', false);
        $users->upsertFromOidc('oid-c', 'c@x', 'C', false, true); // forceAdmin → second admin

        $bob = $users->findByOid('oid-b');
        $r = $users->setTombstoned($bob->id, true);

        $this->assertFalse($r['before']->isTombstoned());
        $this->assertTrue ($r['after']->isTombstoned());
        $this->assertNotNull($r['after']->tombstonedAt);
        $this->assertSame('(former user)', $r['after']->publicEmail());
        $this->assertSame('(former user)', $r['after']->publicDisplayName());
        $this->assertSame('b@x',           $r['after']->email,        'audit-snapshot email is preserved');
        $this->assertSame('B',             $r['after']->displayName,  'audit-snapshot display name is preserved');
        $this->assertFalse($r['after']->isAdmin, 'tombstone forces is_admin=0');
    }

    public function testSetTombstonedClearsAdminEvenForCurrentlyAdminUsers(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-a', 'a@x', 'A', true); // first admin (kept)
        $users->upsertFromOidc('oid-b', 'b@x', 'B', false, true); // forceAdmin → second admin

        $bob = $users->findByOid('oid-b');
        $this->assertTrue($bob->isAdmin);

        $users->setTombstoned($bob->id, true);
        $bob = $users->findByOid('oid-b');
        $this->assertFalse($bob->isAdmin, 'tombstoning an admin must demote them');
        $this->assertSame(1, $users->countAdmins());
    }

    public function testRestoreClearsTombstoneButDoesNotPromote(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
        $users->upsertFromOidc('oid-b', 'b@x', 'B', false, true);

        $bob = $users->findByOid('oid-b');
        $users->setTombstoned($bob->id, true);

        $bob = $users->findByOid('oid-b');
        $this->assertTrue($bob->isTombstoned());
        $this->assertFalse($bob->isAdmin, 'precondition: tombstone demoted bob');

        $r = $users->setTombstoned($bob->id, false);
        $this->assertFalse($r['after']->isTombstoned());
        $this->assertNull($r['after']->tombstonedAt);
        $this->assertFalse($r['after']->isAdmin, 'restore must NOT auto-promote');
    }

    public function testOidcSignInUntombstones(): void
    {
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
        $alice = $users->findByOid('oid-a');
        $users->setTombstoned($alice->id, true);
        $this->assertTrue($users->findByOid('oid-a')->isTombstoned());

        // A successful OIDC sign-in (the upsert path the AuthController calls)
        // must clear the tombstone — the user has demonstrated they still hold
        // the identity, so the `(former user)` label would lie.
        $r = $users->upsertFromOidc('oid-a', 'a@x', 'A', false);
        $this->assertFalse($r['user']->isTombstoned());
    }

    public function testForceAdminAlsoUntombstones(): void
    {
        // Local-admin login path: forceAdmin=true also clears any
        // tombstone, so a tombstoned configured admin can come back.
        $pdo   = $this->makeDb();
        $users = new UserRepository($pdo);

        $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', true, true);
        $admin = $users->findByOid('local:admin@x');
        $users->setTombstoned($admin->id, true);

        $r = $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', false, true);
        $this->assertFalse($r['user']->isTombstoned());
        $this->assertTrue($r['user']->isAdmin, 'forceAdmin path still re-promotes');
    }
}
