const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const { assert } = require('./helpers');

let bundledWebUiHarnessPromise = null;

function getBundledWebUiHarness() {
    if (!bundledWebUiHarnessPromise) {
        const helperPath = path.resolve(__dirname, '..', 'unit', 'helpers', 'web-ui-app-options.mjs');
        bundledWebUiHarnessPromise = import(pathToFileURL(helperPath).href)
            .then(async (mod) => ({
                appOptions: await mod.captureCurrentBundledAppOptions(),
                withGlobalOverrides: mod.withGlobalOverrides
            }));
    }
    return bundledWebUiHarnessPromise;
}

function createIso(baseMs, offsetSeconds) {
    return new Date(baseMs + (offsetSeconds * 1000)).toISOString();
}

function buildCodexSessionRecords(sessionId, messageCount, options = {}) {
    const baseMs = Date.parse(options.baseIso || '2025-04-01T00:00:00.000Z');
    const messageSize = Number.isFinite(options.messageSize)
        ? Math.max(0, Math.floor(options.messageSize))
        : 256;
    const records = [{
        type: 'session_meta',
        payload: {
            id: sessionId,
            cwd: options.cwd || `/tmp/${sessionId}`
        },
        timestamp: createIso(baseMs, 0)
    }];

    for (let i = 0; i < messageCount; i += 1) {
        records.push({
            type: 'response_item',
            payload: {
                type: 'message',
                role: i % 2 === 0 ? 'user' : 'assistant',
                content: `${sessionId}-msg-${String(i).padStart(4, '0')}-` + 'x'.repeat(messageSize)
            },
            timestamp: createIso(baseMs, i + 1)
        });
    }

    return records;
}

function buildCodexSessionRecordsWithLeadingNoise(sessionId, noiseLineCount, messageCount, options = {}) {
    const baseMs = Date.parse(options.baseIso || '2025-04-01T00:00:00.000Z');
    const noiseSize = Number.isFinite(options.noiseSize)
        ? Math.max(256, Math.floor(options.noiseSize))
        : 8192;
    const messageSize = Number.isFinite(options.messageSize)
        ? Math.max(0, Math.floor(options.messageSize))
        : 128;
    const records = [{
        type: 'session_meta',
        payload: {
            id: sessionId,
            cwd: options.cwd || `/tmp/${sessionId}`
        },
        timestamp: createIso(baseMs, 0)
    }];

    for (let i = 0; i < noiseLineCount; i += 1) {
        records.push({
            type: 'response_item',
            payload: {
                type: 'tool_call',
                role: 'assistant',
                content: 'noise-' + String(i).padStart(3, '0') + '-' + 'n'.repeat(noiseSize)
            },
            timestamp: createIso(baseMs, i + 1)
        });
    }

    for (let i = 0; i < messageCount; i += 1) {
        records.push({
            type: 'response_item',
            payload: {
                type: 'message',
                role: i % 2 === 0 ? 'user' : 'assistant',
                content: `${sessionId}-msg-${String(i).padStart(4, '0')}-` + 'x'.repeat(messageSize)
            },
            timestamp: createIso(baseMs, noiseLineCount + i + 1)
        });
    }

    return records;
}

function writeCodexSessionFile(sessionsDir, sessionId, records) {
    const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
    fs.writeFileSync(filePath, records.map((record) => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
    return filePath;
}

function createApiFetch(api) {
    return async function fetch(_url, init = {}) {
        const payload = init && init.body ? JSON.parse(init.body) : {};
        const result = await api(payload.action, payload.params);
        const raw = JSON.stringify(result || {});
        return {
            ok: true,
            status: 200,
            headers: {
                get(name) {
                    return String(name || '').toLowerCase() === 'content-type'
                        ? 'application/json'
                        : '';
                }
            },
            async json() {
                return result;
            },
            async text() {
                return raw;
            }
        };
    };
}

function createLocalStorage() {
    const store = new Map();
    return {
        getItem(key) {
            return store.has(key) ? store.get(key) : null;
        },
        setItem(key, value) {
            store.set(key, String(value));
        },
        removeItem(key) {
            store.delete(key);
        }
    };
}

function createWebUiVm(appOptions) {
    const vm = {
        ...(typeof appOptions.data === 'function' ? appOptions.data() : {}),
        _scheduledFrames: [],
        _idleTasks: [],
        _messages: [],
        $refs: {}
    };

    for (const [name, fn] of Object.entries(appOptions.methods || {})) {
        vm[name] = fn;
    }

    for (const [name, getter] of Object.entries(appOptions.computed || {})) {
        Object.defineProperty(vm, name, {
            configurable: true,
            enumerable: true,
            get() {
                return getter.call(vm);
            }
        });
    }

    vm.$nextTick = function $nextTick(callback) {
        if (typeof callback === 'function') {
            callback();
        }
    };
    vm.scheduleAfterFrame = function scheduleAfterFrame(task) {
        this._scheduledFrames.push(task);
    };
    vm.scheduleIdleTask = function scheduleIdleTask(task) {
        this._idleTasks.push(task);
        return task;
    };
    vm.cancelIdleTask = function cancelIdleTask(handle) {
        this._idleTasks = this._idleTasks.filter((task) => task !== handle);
    };
    vm.showMessage = function showMessage(text, type) {
        this._messages.push({ text: String(text), type: type || 'info' });
    };
    vm.updateSessionTimelineOffset = function updateSessionTimelineOffset() {};
    vm.invalidateSessionTimelineMeasurementCache = function invalidateSessionTimelineMeasurementCache() {};
    vm.scheduleSessionTimelineSync = function scheduleSessionTimelineSync() {};
    vm.cancelSessionTimelineSync = function cancelSessionTimelineSync() {
        this._cancelTimelineSyncCalls = (this._cancelTimelineSyncCalls || 0) + 1;
    };

    return vm;
}

async function flushScheduledFrames(vm) {
    let guard = 0;
    while (
        (Array.isArray(vm._scheduledFrames) && vm._scheduledFrames.length > 0)
        || (Array.isArray(vm._idleTasks) && vm._idleTasks.length > 0)
    ) {
        const task = (Array.isArray(vm._scheduledFrames) && vm._scheduledFrames.length > 0)
            ? vm._scheduledFrames.shift()
            : (Array.isArray(vm._idleTasks) ? vm._idleTasks.shift() : null);
        if (typeof task === 'function') {
            await Promise.resolve(task());
        }
        guard += 1;
        if (guard > 200) {
            throw new Error('scheduled frame queue did not settle');
        }
    }
}

async function waitForCondition(check, label) {
    const deadline = Date.now() + 20000;
    while (Date.now() < deadline) {
        if (check()) {
            return;
        }
        await new Promise((resolve) => setTimeout(resolve, 10));
    }
    throw new Error(label || 'condition not met');
}

module.exports = async function testWebUiSessionBrowser(ctx) {
    const { api, tmpHome } = ctx;
    const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
    const derivedCodexDir = path.join(tmpHome, '.codexmate', 'sessions', 'derived', 'codex');
    fs.mkdirSync(sessionsDir, { recursive: true });

    const denseListSessionCount = 140;
    const hotSessionId = 'session-browser-hot-large-e2e';
    const hotSessionMessageCount = 1325;
    const hugeLineSessionId = 'session-browser-huge-line-e2e';
    const initialHugeLineSessionId = 'session-browser-huge-line-initial-e2e';
    const hydrateZeroSessionId = 'session-browser-hydrate-zero-e2e';
    const hydrateZeroMessageCount = 25;

    for (let i = 0; i < denseListSessionCount; i += 1) {
        const sessionId = `session-browser-list-${String(i).padStart(3, '0')}`;
        const records = buildCodexSessionRecords(sessionId, 4, {
            baseIso: createIso(Date.parse('2025-04-01T00:00:00.000Z'), i * 90),
            messageSize: 96,
            cwd: `/tmp/${sessionId}`
        });
        writeCodexSessionFile(sessionsDir, sessionId, records);
    }

    writeCodexSessionFile(
        sessionsDir,
        hydrateZeroSessionId,
        buildCodexSessionRecordsWithLeadingNoise(hydrateZeroSessionId, 18, hydrateZeroMessageCount, {
            baseIso: '2025-04-01T12:00:00.000Z',
            noiseSize: 9000,
            messageSize: 96,
            cwd: `/tmp/${hydrateZeroSessionId}`
        })
    );

    writeCodexSessionFile(
        sessionsDir,
        hugeLineSessionId,
        [{
            type: 'session_meta',
            payload: { id: hugeLineSessionId, cwd: '/tmp/session-browser-huge-line' },
            timestamp: '2025-04-02T00:00:00.000Z'
        }].concat(Array.from({ length: 3 }, (_, index) => ({
            type: 'response_item',
            payload: {
                type: 'message',
                role: index % 2 === 0 ? 'user' : 'assistant',
                content: `session-browser-huge-line-${index}-` + 'q'.repeat(1300000)
            },
            timestamp: createIso(Date.parse('2025-04-02T00:00:00.000Z'), index + 1)
        })))
    );

    writeCodexSessionFile(sessionsDir, hotSessionId, buildCodexSessionRecords(hotSessionId, hotSessionMessageCount, {
        baseIso: '2025-04-03T00:00:00.000Z',
        messageSize: 2048,
        cwd: '/tmp/session-browser-hot-large'
    }));

    writeCodexSessionFile(
        sessionsDir,
        initialHugeLineSessionId,
        [{
            type: 'session_meta',
            payload: { id: initialHugeLineSessionId, cwd: '/tmp/session-browser-huge-line-initial' },
            timestamp: '2025-04-04T00:00:00.000Z'
        }].concat(Array.from({ length: 2 }, (_, index) => ({
            type: 'response_item',
            payload: {
                type: 'message',
                role: index % 2 === 0 ? 'user' : 'assistant',
                content: `session-browser-huge-line-initial-${index}-` + 'z'.repeat(1300000)
            },
            timestamp: createIso(Date.parse('2025-04-04T00:00:00.000Z'), index + 1)
        })))
    );

    const { appOptions, withGlobalOverrides } = await getBundledWebUiHarness();
    const vm = createWebUiVm(appOptions);
    vm.mainTab = 'settings';
    vm.sessionFilterSource = 'codex';

    const fetch = createApiFetch(api);
    const localStorage = createLocalStorage();

    await withGlobalOverrides({ fetch, localStorage }, async () => {
        vm.switchMainTab('sessions');
        await flushScheduledFrames(vm);
        await waitForCondition(
            () => vm.mainTab === 'sessions' && vm.sessionsLoadedOnce === true && vm.sessionsLoading === false && !!vm.activeSession,
            'session browser switchMainTab did not finish list loading'
        );
        await flushScheduledFrames(vm);
        await waitForCondition(
            () => vm.activeSession && vm.activeSession.sessionId === initialHugeLineSessionId && vm.sessionDetailLoading === false && vm.activeSessionMessages.length > 0,
            'session browser switchMainTab did not finish detail hydration'
        );
    });

    assert(vm.sessionsList.length >= denseListSessionCount + 3, 'session browser should load the large isolated codex dataset');
    assert(
        vm.sessionsList.every((item) => {
            const filePath = String(item && item.filePath ? item.filePath : '');
            return filePath.startsWith(sessionsDir) || filePath.startsWith(derivedCodexDir);
        }),
        'session browser e2e should stay inside the isolated tmp HOME dataset'
    );
    assert(vm.activeSession && vm.activeSession.sessionId === initialHugeLineSessionId, 'session browser should select the newest session from the isolated dataset on tab entry');
    assert(vm.visibleSessionsList.length === vm.sessionListInitialBatchSize, 'session browser should render only the first session list batch initially');
    assert(vm.activeSessionMessages.length > 0, 'session browser should hydrate the initially selected huge-line session on tab entry');
    assert(vm.activeSessionMessages.length <= 2, 'session browser should not duplicate the initial huge-line preview messages');
    assert(vm.activeSessionMessages.every((message) => typeof message.text === 'string' && message.text.length <= 4000), 'session browser should cap huge-line preview text on initial tab entry');
    assert(vm.activeSessionDetailClipped === false, 'session browser should fully recover small huge-line sessions on initial tab entry');
    assert(vm.activeSessionVisibleMessages.length === vm.activeSessionMessages.length, 'session browser should render all truncated huge-line preview messages on initial tab entry');

    const sampleListSessionId = hydrateZeroSessionId;
    const sampleListSession = vm.sessionsList.find((item) => item && item.sessionId === sampleListSessionId);
    assert(sampleListSession, 'session browser should list the sample non-selected session used for messageCount hydration');
    assert(
        vm.activeSession && vm.activeSession.sessionId !== sampleListSessionId,
        'session browser should hydrate messageCount for a non-active session without selecting it'
    );

    await withGlobalOverrides({ fetch, localStorage }, async () => {
        if (typeof vm.hydrateVisibleSessionListMessageCounts === 'function') {
            await vm.hydrateVisibleSessionListMessageCounts();
        }
        await flushScheduledFrames(vm);
        await waitForCondition(
            () => {
                const matched = vm.sessionsList.find((item) => item && item.sessionId === sampleListSessionId);
                return matched && matched.messageCount === hydrateZeroMessageCount;
            },
            'session browser should hydrate messageCount in the list without selecting the session'
        );
    });

    const hotSession = vm.sessionsList.find((item) => item.sessionId === hotSessionId);
    assert(hotSession, 'session browser should list the hot large regression session');

    await withGlobalOverrides({ fetch, localStorage }, async () => {
        await vm.selectSession(hotSession);
        await flushScheduledFrames(vm);
        await waitForCondition(
            () => vm.activeSession && vm.activeSession.sessionId === hotSessionId && vm.sessionDetailLoading === false && vm.activeSessionMessages.length > 0,
            'session browser hot large selection did not finish loading'
        );
    });

    assert(vm.activeSessionMessages.length >= vm.sessionDetailInitialMessageLimit, 'session browser preview should hydrate at least the initial detail window for huge sessions');
    assert(vm.activeSessionDetailClipped === true, 'session browser preview should stay clipped for huge sessions');
    assert(vm.activeSessionVisibleMessages.length === vm.activeSessionMessages.length, 'session browser should render all loaded messages immediately without batching');

    assert(vm.sessionDetailMessageLimit >= vm.sessionDetailInitialMessageLimit, 'session browser detail limit should be at least the initial window');
    assert(vm.activeSessionMessages.length < hotSessionMessageCount, 'session browser should not pull the whole huge session into memory');
    assert(vm.activeSessionDetailClipped === true, 'session browser should remain clipped while more huge-session messages stay on disk');

    const hugeLineSession = vm.sessionsList.find((item) => item.sessionId === hugeLineSessionId);
    assert(hugeLineSession, 'session browser should list the huge-line regression session');

    await withGlobalOverrides({ fetch, localStorage }, async () => {
        await vm.selectSession(hugeLineSession);
        await flushScheduledFrames(vm);
        await waitForCondition(
            () => vm.activeSession && vm.activeSession.sessionId === hugeLineSessionId && vm.sessionDetailLoading === false && vm.activeSessionMessages.length > 0,
            'session browser huge-line selection did not finish loading'
        );
    });

    assert(vm.activeSession && vm.activeSession.sessionId === hugeLineSessionId, 'session browser should switch to the huge-line regression session');
    assert(vm.activeSessionMessages.length > 0, 'session browser should recover huge-line previews through the real UI->API data flow');
    assert(vm.activeSessionMessages.length <= 3, 'session browser should not duplicate huge-line fallback preview messages');
    assert(vm.activeSessionDetailClipped === false, 'session browser should mark the huge-line fallback preview as fully recovered');
    assert(vm.activeSessionVisibleMessages.length === vm.activeSessionMessages.length, 'session browser should render all recovered huge-line messages without overfetching');
};
