// ==========================================
// Auth Resilience Tests
// ==========================================
// Validates that auth failure during startup
// results in a redirect to login, NOT a broken
// main UI with 401 errors everywhere.
//
// Bug: Profile returned 200 (token on edge of
// expiry), startApp() proceeded, then every
// subsequent call 401'd. User saw broken UI
// instead of login screen.
//
// Run: node --test src/js/__tests__/auth-resilience.test.js
// ==========================================

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');

// ── Auth state model ────────────────────────

describe('Auth state transitions', () => {

    // Simulates the API client's auth lifecycle
    function makeAuthState() {
        return {
            accessToken: null,
            refreshToken: null,
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; this.refreshToken = null; },
            setTokens(access, refresh) { this.accessToken = access; this.refreshToken = refresh; },
        };
    }

    it('isAuthed reflects token presence', () => {
        const auth = makeAuthState();
        assert.equal(auth.isAuthed, false);
        auth.setTokens('tok_access', 'tok_refresh');
        assert.equal(auth.isAuthed, true);
    });

    it('clearTokens makes isAuthed false', () => {
        const auth = makeAuthState();
        auth.setTokens('tok_access', 'tok_refresh');
        assert.equal(auth.isAuthed, true);
        auth.clearTokens();
        assert.equal(auth.isAuthed, false);
    });

    it('expired token string is still truthy (the edge case)', () => {
        const auth = makeAuthState();
        // An expired JWT is still a non-empty string — isAuthed returns true
        // This is BY DESIGN: the server validates, not the client
        const expiredJWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.fake';
        auth.setTokens(expiredJWT, 'tok_refresh');
        assert.equal(auth.isAuthed, true, 'expired JWT is still truthy');
    });
});

// ── Startup auth guard ──────────────────────

describe('startApp auth guard', () => {

    // This models the critical startup sequence:
    //   init():
    //     if (isAuthed) → getProfile() → if ok → startApp()
    //   startApp():
    //     loadSettings() → loadChats() → fetchModels()
    //     *** AUTH GUARD CHECK HERE ***
    //     if (!isAuthed) → showSplash, return
    //     else → render UI

    it('proceeds when auth is valid throughout startup', () => {
        let splashShown = false;
        let uiRendered = false;

        const auth = { isAuthed: true };
        // Simulate: loadChats succeeds, auth stays valid
        const loadChats = () => { /* no 401 */ };
        const fetchModels = () => { /* no 401 */ };

        // startApp logic
        loadChats();
        fetchModels();
        if (!auth.isAuthed) {
            splashShown = true;
        } else {
            uiRendered = true;
        }

        assert.equal(uiRendered, true, 'UI should render when auth is valid');
        assert.equal(splashShown, false);
    });

    it('returns to login when auth is lost during startup', () => {
        let splashShown = false;
        let uiRendered = false;

        const auth = {
            accessToken: 'tok',
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; },
        };

        // Simulate: loadChats gets 401, refresh fails, clearTokens called
        const loadChats = () => {
            // _authed → 401 → refresh() → fails → clearTokens()
            auth.clearTokens();
        };
        const fetchModels = () => { /* also 401 but tokens already cleared */ };

        loadChats();
        fetchModels();

        // THE FIX: guard check after critical loads
        if (!auth.isAuthed) {
            splashShown = true;
        } else {
            uiRendered = true;
        }

        assert.equal(splashShown, true, 'must return to login when auth lost');
        assert.equal(uiRendered, false, 'must NOT render UI when auth lost');
    });

    it('detects auth loss even when only one call triggers it', () => {
        const auth = {
            accessToken: 'tok',
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; },
        };

        let splashShown = false;

        // loadSettings succeeds (token barely valid)
        // loadChats triggers 401 → clearTokens
        const loadSettings = () => { /* ok */ };
        const loadChats = () => { auth.clearTokens(); };
        const fetchModels = () => { /* tokens already gone, silently fails */ };

        loadSettings();
        loadChats();
        fetchModels();

        if (!auth.isAuthed) splashShown = true;

        assert.equal(splashShown, true, 'single 401 during startup must trigger login redirect');
    });
});

// ── 401 retry + refresh behavior ─────────────

describe('401 retry with refresh', () => {

    it('successful refresh preserves auth', () => {
        const auth = {
            accessToken: 'expired',
            refreshToken: 'valid_refresh',
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; this.refreshToken = null; },
            refresh() {
                // Simulate successful refresh
                this.accessToken = 'new_valid_token';
                return true;
            },
        };

        // _authed: first call 401, refresh succeeds, retry succeeds
        const result401 = { status: 401 };
        if (result401.status === 401 && auth.refreshToken) {
            auth.refresh();
        }

        assert.equal(auth.isAuthed, true, 'auth preserved after successful refresh');
        assert.equal(auth.accessToken, 'new_valid_token');
    });

    it('failed refresh clears auth', () => {
        const auth = {
            accessToken: 'expired',
            refreshToken: 'also_expired',
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; this.refreshToken = null; },
            refresh() {
                // Simulate failed refresh (401 on refresh endpoint)
                this.clearTokens();
                return false;
            },
        };

        const result401 = { status: 401 };
        if (result401.status === 401 && auth.refreshToken) {
            auth.refresh();
        }

        assert.equal(auth.isAuthed, false, 'auth cleared after failed refresh');
    });
});

// ── Init flow: profile check ─────────────────

describe('init() profile gate', () => {

    it('clears tokens when profile returns 401', () => {
        const auth = {
            accessToken: 'expired',
            refreshToken: 'expired_too',
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; this.refreshToken = null; },
        };

        // Simulate getProfile throwing (as _authed does after failed refresh)
        const profileFailed = true;
        if (profileFailed) auth.clearTokens();

        assert.equal(auth.isAuthed, false, 'tokens cleared on profile failure');
    });

    it('profile success + subsequent 401 = the edge case bug', () => {
        // This is the exact scenario from the bug report:
        // 1. Profile returns 200 (token barely valid, 5ms)
        // 2. init() proceeds to startApp()
        // 3. loadChats() returns 401 (token expired in the 200ms gap)
        // 4. Without the guard, user sees broken UI

        const auth = {
            accessToken: 'barely_valid',
            get isAuthed() { return !!this.accessToken; },
            clearTokens() { this.accessToken = null; },
        };

        // Profile succeeds (token still valid)
        const profileOk = true;
        assert.equal(profileOk, true);
        assert.equal(auth.isAuthed, true, 'auth valid after profile');

        // startApp: loadChats fails, clears tokens
        auth.clearTokens();

        // THE GUARD: must catch this
        assert.equal(auth.isAuthed, false, 'auth gone after loadChats 401');
        // Without guard: UI renders. With guard: splash shown.
    });
});

// ── Boot-time 401 redirect suppression ──────
// v0.37.14: Stale cookie blank-login-page fix.
// When the login page boots the SDK with stale localStorage tokens,
// the REST client's 401 handler must NOT redirect to /login (we're
// already there). boot() handles 401 gracefully on its own.

describe('on401Failure redirect suppression', () => {

    // Model of the _on401Failure guard logic
    function make401Handler() {
        let _booting = false;
        let redirectedTo = null;

        return {
            set booting(v) { _booting = v; },
            get booting() { return _booting; },
            get redirectedTo() { return redirectedTo; },
            reset() { redirectedTo = null; },

            on401Failure(pathname, basePath) {
                // Mirrors the real _on401Failure logic
                if (_booting) return;
                const loginPath = basePath + '/login';
                const here = pathname.replace(/\/+$/, '');
                if (here === loginPath) return;
                redirectedTo = loginPath;
            },
        };
    }

    it('suppresses redirect during boot', () => {
        const h = make401Handler();
        h.booting = true;
        h.on401Failure('/chat', '');
        assert.equal(h.redirectedTo, null, 'must NOT redirect during boot');
    });

    it('allows redirect after boot completes', () => {
        const h = make401Handler();
        h.booting = true;
        h.on401Failure('/chat', '');
        assert.equal(h.redirectedTo, null);

        h.booting = false;
        h.on401Failure('/chat', '');
        assert.equal(h.redirectedTo, '/login', 'must redirect after boot');
    });

    it('suppresses redirect when already on /login', () => {
        const h = make401Handler();
        h.on401Failure('/login', '');
        assert.equal(h.redirectedTo, null, 'must NOT redirect to /login from /login');
    });

    it('suppresses redirect when on /login with trailing slash', () => {
        const h = make401Handler();
        h.on401Failure('/login/', '');
        assert.equal(h.redirectedTo, null, 'must NOT redirect from /login/');
    });

    it('handles basePath correctly', () => {
        const h = make401Handler();
        h.on401Failure('/app/login', '/app');
        assert.equal(h.redirectedTo, null, 'must NOT redirect from basePath + /login');
    });

    it('redirects from non-login paths normally', () => {
        const h = make401Handler();
        h.on401Failure('/chat', '');
        assert.equal(h.redirectedTo, '/login');
    });

    it('redirects from non-login paths with basePath', () => {
        const h = make401Handler();
        h.on401Failure('/app/chat', '/app');
        assert.equal(h.redirectedTo, '/app/login');
    });
});
