<?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\Auth;

use App\Auth\OidcClaims;
use App\Tests\TestCase;
use stdClass;

/**
 * R01-N18 — pin the email-resolution rules. The function decides what
 * string ends up in `users.email` and `audit_log.user_email` for OIDC
 * users. Identity is keyed by `oid`, which is unaffected.
 */
final class OidcClaimsTest extends TestCase
{
    private const OID = '00000000-0000-0000-0000-000000000abc';

    public function testTrustsEmailWhenExplicitlyVerified(): void
    {
        $c = new stdClass();
        $c->email          = 'alice@example.com';
        $c->email_verified = true;

        self::assertSame(
            'alice@example.com',
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testTrustsEmailWhenVerifiedFlagIsAbsent(): void
    {
        // Entra v2.0 work/school accounts do not emit email_verified;
        // the email comes from the directory and is server-controlled.
        $c = new stdClass();
        $c->email = 'alice@example.com';

        self::assertSame(
            'alice@example.com',
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testFallsBackWhenIssuerExplicitlyUnverified(): void
    {
        $c = new stdClass();
        $c->email          = 'spoofed@example.com';
        $c->email_verified = false;

        self::assertSame(
            'entra:' . self::OID,
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testNeverFallsBackToPreferredUsername(): void
    {
        // The actual hazard the finding flagged: preferred_username may
        // be user-controlled. Even when email is missing entirely, the
        // resolver must NOT pick it up.
        $c = new stdClass();
        $c->preferred_username = 'attacker@victim.example';

        self::assertSame(
            'entra:' . self::OID,
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testEmptyEmailFallsBack(): void
    {
        $c = new stdClass();
        $c->email          = '';
        $c->email_verified = true;

        self::assertSame(
            'entra:' . self::OID,
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testWhitespaceOnlyEmailFallsBack(): void
    {
        $c = new stdClass();
        $c->email          = "   \t  ";
        $c->email_verified = true;

        self::assertSame(
            'entra:' . self::OID,
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testEmailIsTrimmed(): void
    {
        $c = new stdClass();
        $c->email = '  alice@example.com  ';

        self::assertSame(
            'alice@example.com',
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testNoClaimsAtAllFallsBack(): void
    {
        $c = new stdClass();

        self::assertSame(
            'entra:' . self::OID,
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testTruthyButNonBooleanVerifiedStillTrusted(): void
    {
        // Per the OIDC core spec email_verified should be boolean. Some
        // IdPs ship strings; we only treat literal `false` as a negative
        // signal — anything else is "no negative signal" and we trust
        // the email.
        $c = new stdClass();
        $c->email          = 'alice@example.com';
        $c->email_verified = 'false'; // string, not bool false

        self::assertSame(
            'alice@example.com',
            OidcClaims::resolveEmail($c, self::OID)
        );
    }

    public function testNonScalarEmailTreatedAsMissing(): void
    {
        // Defence against unexpected JWT decoder shapes (e.g. nested
        // objects, arrays). is_scalar() guards the cast.
        $c = new stdClass();
        $c->email = ['alice@example.com'];

        self::assertSame(
            'entra:' . self::OID,
            OidcClaims::resolveEmail($c, self::OID)
        );
    }
}
