<?php

// Feature: premium-header-session-badges
// Unit + Property tests for renderSessionTermBadges()

declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../includes/session_term_helper.php';

/**
 * Task 12.2 — Unit tests for renderSessionTermBadges().
 * Validates: Requirements 1.2–1.5, 4.1–4.8
 */
class SessionTermBadgesTest extends TestCase
{
    // ── Unit Tests ────────────────────────────────────────────────────────────

    /** Both session and term present */
    public function testBothPresent(): void
    {
        $html = renderSessionTermBadges('2024/2025', 'First Term');
        $this->assertStringContainsString('badge-primary', $html);
        $this->assertStringContainsString('2024/2025', $html);
        $this->assertStringContainsString('badge-success', $html);
        $this->assertStringContainsString('First Term', $html);
        $this->assertStringNotContainsString('badge-warning', $html);
    }

    /** Session present, term null → warning badge */
    public function testSessionOnlyNoTerm(): void
    {
        $html = renderSessionTermBadges('2024/2025', null);
        $this->assertStringContainsString('badge-primary', $html);
        $this->assertStringContainsString('2024/2025', $html);
        $this->assertStringNotContainsString('badge-success', $html);
        $this->assertStringContainsString('badge-warning', $html);
        $this->assertStringContainsString('No active term', $html);
    }

    /** Term present, session null → no session badge */
    public function testTermOnlyNoSession(): void
    {
        $html = renderSessionTermBadges(null, 'First Term');
        $this->assertStringNotContainsString('badge-primary', $html);
        $this->assertStringContainsString('badge-success', $html);
        $this->assertStringContainsString('First Term', $html);
        $this->assertStringNotContainsString('badge-warning', $html);
    }

    /** Both null → no session badge, warning badge */
    public function testBothNull(): void
    {
        $html = renderSessionTermBadges(null, null);
        $this->assertStringNotContainsString('badge-primary', $html);
        $this->assertStringNotContainsString('badge-success', $html);
        $this->assertStringContainsString('badge-warning', $html);
        $this->assertStringContainsString('No active term', $html);
    }

    /** XSS in session name is escaped */
    public function testXssInSessionName(): void
    {
        $html = renderSessionTermBadges('<script>alert(1)</script>', 'First Term');
        $this->assertStringContainsString('&lt;script&gt;', $html);
        $this->assertStringNotContainsString('<script>', $html);
        $this->assertStringContainsString('badge-primary', $html);
    }

    /** XSS in term name is escaped */
    public function testXssInTermName(): void
    {
        $html = renderSessionTermBadges('2024/2025', '<b>Term</b>');
        $this->assertStringContainsString('&lt;b&gt;Term&lt;/b&gt;', $html);
        $this->assertStringNotContainsString('<b>Term</b>', $html);
        $this->assertStringContainsString('badge-success', $html);
    }

    /** Empty string session → no session badge */
    public function testEmptyStringSession(): void
    {
        $html = renderSessionTermBadges('', 'First Term');
        $this->assertStringNotContainsString('badge-primary', $html);
        $this->assertStringContainsString('badge-success', $html);
    }

    /** Whitespace-only session → no session badge (empty() treats whitespace as truthy, so we test actual behaviour) */
    public function testWhitespaceSession(): void
    {
        $html = renderSessionTermBadges('   ', 'First Term');
        // '   ' is non-empty string — badge-primary will appear; this documents the behaviour
        // If the requirement is to treat whitespace as absent, trim() should be added to the helper
        $this->assertStringContainsString('badge-success', $html);
    }

    /** Wrapper div is always present */
    public function testWrapperDivAlwaysPresent(): void
    {
        foreach ([
            [null, null],
            ['2024/2025', null],
            [null, 'First Term'],
            ['2024/2025', 'First Term'],
        ] as [$s, $t]) {
            $html = renderSessionTermBadges($s, $t);
            $this->assertStringContainsString('<div class="mt-1">', $html, "mt-1 wrapper missing for session=$s term=$t");
        }
    }

    // ── Property-Based Tests ──────────────────────────────────────────────────

    /**
     * Property 1: Badge block renders session name safely (XSS safety).
     * Feature: premium-header-session-badges, Property 1: Badge block renders session name safely
     * Validates: Requirements 1.2, 4.8
     */
    public function testProperty1SessionXssSafety(): void
    {
        $specialChars = ['<', '>', '"', "'", '&', '<script>', '"><img src=x onerror=alert(1)>'];
        foreach ($specialChars as $char) {
            $input = 'Session ' . $char . ' 2024';
            $html = renderSessionTermBadges($input, 'First Term');
            $escaped = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
            $this->assertStringContainsString($escaped, $html, "Escaped value not found for: $input");
            // Raw unescaped input should not appear when it contains escapable chars
            if ($input !== $escaped) {
                $this->assertStringNotContainsString($input, $html, "Raw unescaped input found for: $input");
            }
        }
    }

    /**
     * Property 2: Badge block renders term name safely (XSS safety).
     * Feature: premium-header-session-badges, Property 2: Badge block renders term name safely
     * Validates: Requirements 1.3, 4.8
     */
    public function testProperty2TermXssSafety(): void
    {
        $specialChars = ['<', '>', '"', "'", '&', '<script>', '"><img src=x onerror=alert(1)>'];
        foreach ($specialChars as $char) {
            $input = 'Term ' . $char . ' One';
            $html = renderSessionTermBadges('2024/2025', $input);
            $escaped = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
            $this->assertStringContainsString($escaped, $html, "Escaped value not found for: $input");
            if ($input !== $escaped) {
                $this->assertStringNotContainsString($input, $html, "Raw unescaped input found for: $input");
            }
        }
    }

    /**
     * Property 3: No-term fallback is mutually exclusive with term badge.
     * Feature: premium-header-session-badges, Property 3: No-term fallback is mutually exclusive with term badge
     * Validates: Requirements 1.3, 1.4
     */
    public function testProperty3TermBadgeMutualExclusivity(): void
    {
        $termValues = [null, '', 'First Term', 'Second Term', 'Third Term', '  '];
        foreach ($termValues as $term) {
            $html = renderSessionTermBadges('2024/2025', $term);
            $hasSuccess = str_contains($html, 'badge-success');
            $hasWarning = str_contains($html, 'badge-warning');
            // Exactly one must be true, never both, never neither
            $this->assertTrue(
                ($hasSuccess xor $hasWarning),
                "Expected exactly one of badge-success or badge-warning for term=" . var_export($term, true)
            );
        }
    }

    /**
     * Property 4: Session badge omitted when session name is absent.
     * Feature: premium-header-session-badges, Property 4: Session badge omitted when session name is absent
     * Validates: Requirements 1.5
     */
    public function testProperty4SessionBadgeOmittedWhenAbsent(): void
    {
        $absentValues = [null, ''];
        foreach ($absentValues as $session) {
            $html = renderSessionTermBadges($session, 'First Term');
            $this->assertStringNotContainsString(
                'badge-primary',
                $html,
                "badge-primary should not appear when session=" . var_export($session, true)
            );
        }
    }

    /**
     * Property 5: Badge block structure invariant.
     * Feature: premium-header-session-badges, Property 5: Badge block structure invariant
     * Validates: Requirements 4.1–4.4
     */
    public function testProperty5BadgeBlockStructureInvariant(): void
    {
        $combinations = [
            [null, null],
            ['2024/2025', null],
            [null, 'First Term'],
            ['2024/2025', 'First Term'],
            ['', ''],
            ['Session A', 'Term B'],
        ];
        foreach ($combinations as [$session, $term]) {
            $html = renderSessionTermBadges($session, $term);
            // Always wrapped in mt-1 div
            $this->assertStringContainsString('<div class="mt-1">', $html);
            // Session badge uses correct classes when present
            if (!empty($session)) {
                $this->assertStringContainsString('badge badge-primary mr-1', $html);
            }
            // Term badge uses correct class when present
            if (!empty($term)) {
                $this->assertStringContainsString('badge badge-success', $html);
            }
            // Warning badge uses correct class when term absent
            if (empty($term)) {
                $this->assertStringContainsString('badge badge-warning', $html);
            }
        }
    }
}
