/**
 * Unit tests for session cleanup race conditions.
 *
 * Covers:
 * 1. Tab reaper -> empty session cleanup (with _closing flag)
 * 2. getSession() skips sessions marked _closing
 * 3. YT transcript cleanup uses context.pages() instead of tabGroups
 * 4. Session expiry sets _closing before teardown
 */

describe('session cleanup after tab reaper', () => {
  // Simulate the reaper loop logic from server.js (with _closing flag)
  function runTabReaper({ sessions, TAB_INACTIVITY_MS, destroyTab, onSessionEmpty }) {
    const now = Date.now();
    for (const [userId, session] of sessions) {
      for (const [listItemId, group] of session.tabGroups) {
        for (const [tabId, tabState] of group) {
          if (!tabState._lastReaperCheck) {
            tabState._lastReaperCheck = now;
            tabState._lastReaperToolCalls = tabState.toolCalls;
            continue;
          }
          if (tabState.toolCalls === tabState._lastReaperToolCalls) {
            const idleMs = now - tabState._lastReaperCheck;
            if (idleMs >= TAB_INACTIVITY_MS) {
              destroyTab(tabId);
              group.delete(tabId);
            }
          } else {
            tabState._lastReaperCheck = now;
            tabState._lastReaperToolCalls = tabState.toolCalls;
          }
        }
        if (group.size === 0) {
          session.tabGroups.delete(listItemId);
        }
      }
      if (session.tabGroups.size === 0) {
        session._closing = true;
        onSessionEmpty(userId);
        sessions.delete(userId);
      }
    }
  }

  function makeSession(tabs) {
    const tabGroups = new Map();
    const group = new Map();
    for (const [tabId, tabState] of Object.entries(tabs)) {
      group.set(tabId, { toolCalls: 0, ...tabState });
    }
    tabGroups.set('list-1', group);
    return { tabGroups, lastAccess: Date.now() };
  }

  test('empty session is cleaned up when all tabs are reaped', () => {
    const past = Date.now() - 600_000; // 10 min ago
    const sessions = new Map();
    sessions.set('user-1', makeSession({
      'tab-1': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 0 },
      'tab-2': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 0 },
    }));

    const destroyed = [];
    const emptied = [];

    runTabReaper({
      sessions,
      TAB_INACTIVITY_MS: 300_000,
      destroyTab: (id) => destroyed.push(id),
      onSessionEmpty: (userId) => emptied.push(userId),
    });

    expect(destroyed).toEqual(['tab-1', 'tab-2']);
    expect(emptied).toEqual(['user-1']);
    expect(sessions.size).toBe(0);
  });

  test('reaped session gets _closing flag set before deletion', () => {
    const past = Date.now() - 600_000;
    const sessions = new Map();
    const session = makeSession({
      'tab-1': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 0 },
    });
    sessions.set('user-1', session);

    let closingFlagAtCallback = null;
    runTabReaper({
      sessions,
      TAB_INACTIVITY_MS: 300_000,
      destroyTab: () => {},
      onSessionEmpty: () => { closingFlagAtCallback = session._closing; },
    });

    // _closing should be set BEFORE the onSessionEmpty callback
    expect(closingFlagAtCallback).toBe(true);
  });

  test('session with active tabs is NOT cleaned up', () => {
    const past = Date.now() - 600_000;
    const sessions = new Map();
    sessions.set('user-1', makeSession({
      'tab-1': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 0 },
      'tab-2': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 5 }, // active
    }));

    const destroyed = [];
    const emptied = [];

    runTabReaper({
      sessions,
      TAB_INACTIVITY_MS: 300_000,
      destroyTab: (id) => destroyed.push(id),
      onSessionEmpty: (userId) => emptied.push(userId),
    });

    expect(destroyed).toEqual(['tab-1']);
    expect(emptied).toEqual([]);
    expect(sessions.size).toBe(1);
    const session = sessions.get('user-1');
    expect(session._closing).toBeUndefined();
    expect(session.tabGroups.get('list-1').has('tab-2')).toBe(true);
  });

  test('multiple sessions: only empty ones are cleaned up', () => {
    const past = Date.now() - 600_000;
    const sessions = new Map();
    sessions.set('user-1', makeSession({
      'tab-1': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 0 },
    }));
    sessions.set('user-2', makeSession({
      'tab-2': { _lastReaperCheck: past, _lastReaperToolCalls: 0, toolCalls: 3 }, // active
    }));

    const emptied = [];

    runTabReaper({
      sessions,
      TAB_INACTIVITY_MS: 300_000,
      destroyTab: () => {},
      onSessionEmpty: (userId) => emptied.push(userId),
    });

    expect(emptied).toEqual(['user-1']);
    expect(sessions.size).toBe(1);
    expect(sessions.has('user-2')).toBe(true);
  });

  test('tabs not yet checked are skipped (first pass initializes reaper state)', () => {
    const sessions = new Map();
    sessions.set('user-1', makeSession({
      'tab-1': { toolCalls: 0 }, // no _lastReaperCheck
    }));

    const destroyed = [];
    const emptied = [];

    runTabReaper({
      sessions,
      TAB_INACTIVITY_MS: 300_000,
      destroyTab: (id) => destroyed.push(id),
      onSessionEmpty: (userId) => emptied.push(userId),
    });

    expect(destroyed).toEqual([]);
    expect(emptied).toEqual([]);
    expect(sessions.size).toBe(1);
  });
});

describe('getSession _closing flag handling', () => {
  // Simulate getSession logic from server.js
  function getSession(sessions, userId, createContext) {
    const key = String(userId);
    let session = sessions.get(key);

    if (session) {
      if (session._closing) {
        session = null;
      } else {
        try {
          session.context.pages();
        } catch {
          sessions.delete(key);
          session = null;
        }
      }
    }

    if (!session) {
      const context = createContext();
      session = { context, tabGroups: new Map(), lastAccess: Date.now() };
      sessions.set(key, session);
    }
    session.lastAccess = Date.now();
    return session;
  }

  test('returns existing session when context is alive', () => {
    const sessions = new Map();
    const existingContext = { pages: () => [] };
    sessions.set('user-1', { context: existingContext, tabGroups: new Map(), lastAccess: 0 });

    const result = getSession(sessions, 'user-1', () => { throw new Error('should not create'); });
    expect(result.context).toBe(existingContext);
  });

  test('skips session with _closing flag and creates new one', () => {
    const sessions = new Map();
    const oldContext = { pages: () => [] };
    sessions.set('user-1', { context: oldContext, tabGroups: new Map(), lastAccess: 0, _closing: true });

    const newContext = { pages: () => [] };
    const result = getSession(sessions, 'user-1', () => newContext);

    expect(result.context).toBe(newContext);
    expect(result.context).not.toBe(oldContext);
    expect(result._closing).toBeUndefined();
    // Old entry is replaced in the map
    expect(sessions.get('user-1').context).toBe(newContext);
  });

  test('recreates session when context.pages() throws', () => {
    const sessions = new Map();
    const deadContext = { pages: () => { throw new Error('context closed'); } };
    sessions.set('user-1', { context: deadContext, tabGroups: new Map(), lastAccess: 0 });

    const newContext = { pages: () => [] };
    const result = getSession(sessions, 'user-1', () => newContext);

    expect(result.context).toBe(newContext);
  });

  test('creates fresh session when none exists', () => {
    const sessions = new Map();
    const newContext = { pages: () => [] };
    const result = getSession(sessions, 'user-1', () => newContext);

    expect(result.context).toBe(newContext);
    expect(sessions.has('user-1')).toBe(true);
  });
});

describe('YT transcript session cleanup', () => {
  // Simulate the finally-block cleanup logic from browserTranscript
  function ytCleanup(sessions, ytKey, contextPagesResult, contextPagesThrows) {
    const ytSession = sessions.get(ytKey);
    if (ytSession && !ytSession._closing) {
      try {
        if (contextPagesThrows) throw new Error('context closed');
        const remainingPages = contextPagesResult;
        if (remainingPages.length === 0) {
          ytSession._closing = true;
          // context.close() would be called here
          sessions.delete(ytKey);
        }
      } catch {
        sessions.delete(ytKey);
      }
    }
  }

  test('does NOT close session when other pages are still open', () => {
    const sessions = new Map();
    const session = { context: {}, tabGroups: new Map(), lastAccess: Date.now() };
    sessions.set('__yt_transcript__', session);

    // Another transcript request still has a page open
    ytCleanup(sessions, '__yt_transcript__', [{ /* page */ }], false);

    expect(sessions.has('__yt_transcript__')).toBe(true);
    expect(session._closing).toBeUndefined();
  });

  test('closes session when no pages remain', () => {
    const sessions = new Map();
    const session = { context: {}, tabGroups: new Map(), lastAccess: Date.now() };
    sessions.set('__yt_transcript__', session);

    ytCleanup(sessions, '__yt_transcript__', [], false);

    expect(sessions.has('__yt_transcript__')).toBe(false);
    expect(session._closing).toBe(true);
  });

  test('cleans up map entry when context is already dead', () => {
    const sessions = new Map();
    const session = { context: {}, tabGroups: new Map(), lastAccess: Date.now() };
    sessions.set('__yt_transcript__', session);

    ytCleanup(sessions, '__yt_transcript__', [], true /* throws */);

    expect(sessions.has('__yt_transcript__')).toBe(false);
  });

  test('skips cleanup when session is already _closing', () => {
    const sessions = new Map();
    const session = { context: {}, tabGroups: new Map(), lastAccess: Date.now(), _closing: true };
    sessions.set('__yt_transcript__', session);

    ytCleanup(sessions, '__yt_transcript__', [], false);

    // Session is still in the map (another cleanup path owns it)
    expect(sessions.has('__yt_transcript__')).toBe(true);
  });

  test('concurrent requests: first closer sees pages, second sees empty', () => {
    const sessions = new Map();
    const session = { context: {}, tabGroups: new Map(), lastAccess: Date.now() };
    sessions.set('__yt_transcript__', session);

    // Request A finishes first -- request B still has a page
    ytCleanup(sessions, '__yt_transcript__', [{ /* B's page */ }], false);
    expect(sessions.has('__yt_transcript__')).toBe(true);
    expect(session._closing).toBeUndefined();

    // Request B finishes -- no pages left
    ytCleanup(sessions, '__yt_transcript__', [], false);
    expect(sessions.has('__yt_transcript__')).toBe(false);
    expect(session._closing).toBe(true);
  });
});

describe('session expiry _closing flag', () => {
  // Simulate session expiry logic from server.js
  function runSessionExpiry({ sessions, SESSION_TIMEOUT_MS, onExpired }) {
    const now = Date.now();
    for (const [userId, session] of sessions) {
      if (now - session.lastAccess > SESSION_TIMEOUT_MS) {
        session._closing = true;
        onExpired(userId);
        sessions.delete(userId);
      }
    }
  }

  test('expired session gets _closing flag before deletion', () => {
    const past = Date.now() - 600_000;
    const sessions = new Map();
    const session = { tabGroups: new Map(), lastAccess: past };
    sessions.set('user-1', session);

    let closingFlagAtCallback = null;
    runSessionExpiry({
      sessions,
      SESSION_TIMEOUT_MS: 300_000,
      onExpired: () => { closingFlagAtCallback = session._closing; },
    });

    expect(closingFlagAtCallback).toBe(true);
    expect(sessions.size).toBe(0);
  });

  test('active session is NOT expired or flagged', () => {
    const sessions = new Map();
    const session = { tabGroups: new Map(), lastAccess: Date.now() };
    sessions.set('user-1', session);

    runSessionExpiry({
      sessions,
      SESSION_TIMEOUT_MS: 300_000,
      onExpired: () => { throw new Error('should not expire'); },
    });

    expect(sessions.size).toBe(1);
    expect(session._closing).toBeUndefined();
  });
});
