import { afterEach, describe, expect, it, vi } from 'vitest';

import { createSessionAuthStorage } from '../session-auth';

class MemoryStorage {
  values = new Map<string, string>();
  getItem(key: string) {
    return this.values.get(key) ?? null;
  }
  setItem(key: string, value: string) {
    this.values.set(key, value);
  }
}

class Channel extends EventTarget {
  static peers = new Set<Channel>();
  constructor() {
    super();
    Channel.peers.add(this);
  }
  close() {
    Channel.peers.delete(this);
  }
  postMessage(data: unknown) {
    for (const peer of Channel.peers) {
      if (peer !== this) queueMicrotask(() => peer.dispatchEvent(new MessageEvent('message', { data })));
    }
  }
}

const value = (accessToken: null | string) => JSON.stringify({ accessToken, refreshToken: null });
const disposals: (() => void)[] = [];
function tab(persistent: MemoryStorage, session = new MemoryStorage(), connected = true) {
  const events = new EventTarget();
  const onRemoteChange = vi.fn();
  const channel = connected ? new Channel() : undefined;
  const auth = createSessionAuthStorage({
    key: 'app-core-access',
    persistent,
    session,
    channel: channel as unknown as BroadcastChannel,
    events: events as Window,
    onRemoteChange,
  });
  disposals.push(auth.dispose);
  return { auth, session, events, onRemoteChange, channel };
}
async function start(auth: ReturnType<typeof tab>['auth']) {
  const ready = auth.start();
  await vi.advanceTimersByTimeAsync(400);
  await ready;
}

afterEach(() => {
  for (const dispose of disposals.splice(0)) dispose();
  Channel.peers.clear();
  vi.useRealTimers();
});

describe('session authentication', () => {
  it('keeps credentials out of persistent storage and restores on refresh', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('test-token'));
    expect([...persistent.values.values()].join(',')).not.toContain('test-token');
    a.auth.dispose();
    const refreshed = tab(persistent, a.session);
    await start(refreshed.auth);
    expect(refreshed.auth.getItem()).toBe(value('test-token'));
  });

  it('shares the same credential with a new tab without another login', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('same-token'));
    const b = tab(persistent);
    await start(b.auth);
    expect(b.auth.getItem()).toBe(value('same-token'));
    expect(b.onRemoteChange).not.toHaveBeenCalled();
    expect(a.onRemoteChange).not.toHaveBeenCalled();
  });

  it('broadcasts login to an already open login page and logout to all peers', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    const b = tab(persistent);
    await start(a.auth);
    await start(b.auth);
    a.auth.setItem('', value('token'));
    await vi.advanceTimersByTimeAsync(0);
    expect(b.auth.getItem()).toBe(value('token'));
    expect(b.onRemoteChange).toHaveBeenCalledTimes(1);
    a.auth.setItem('', value(null));
    await vi.advanceTimersByTimeAsync(0);
    expect(b.auth.getItem()).toBe(value(null));
    expect(b.onRemoteChange).toHaveBeenCalledTimes(2);
    expect(a.onRemoteChange).not.toHaveBeenCalled();
  });

  it('has no credential after all pages close and a fresh session starts', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('token'));
    a.auth.dispose();
    const b = tab(persistent);
    await start(b.auth);
    expect(b.auth.getItem()).toBeNull();
  });

  it('rejects a delayed pre-logout response', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('old-token'));
    const oldRevision = persistent.getItem('app-core-access-revision');
    a.auth.setItem('', value(null));
    a.channel!.dispatchEvent(
      new MessageEvent('message', {
        data: {
          type: 'snapshot',
          snapshot: { revision: oldRevision, value: value('old-token') },
        },
      }),
    );
    expect(a.auth.getItem()).toBe(value(null));
  });

  it('invalidates a sleeping tab on focus and does not let stale writes revoke a newer session', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('old-token'));
    const sleeping = tab(persistent, a.session, false);
    await start(sleeping.auth);
    a.auth.setItem('', value('new-token'));
    const revision = persistent.getItem('app-core-access-revision');
    sleeping.auth.setItem('', value(null));
    expect(persistent.getItem('app-core-access-revision')).toBe(revision);
    sleeping.events.dispatchEvent(new Event('focus'));
    await vi.advanceTimersByTimeAsync(300);
    expect(sleeping.auth.getItem()).toBeNull();
    expect(sleeping.onRemoteChange).toHaveBeenCalledTimes(1);
  });

  it('rejects a saved session revoked while its tab was closed', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('token'));
    const saved = new MemoryStorage();
    saved.values = new Map(a.session.values);
    a.auth.setItem('', value(null));
    a.auth.dispose();
    const restored = tab(persistent, saved);
    await start(restored.auth);
    expect(restored.auth.getItem()).toBeNull();
  });

  it('documents that browser-restored session storage can restore authentication', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const a = tab(persistent);
    await start(a.auth);
    a.auth.setItem('', value('token'));
    a.auth.dispose();
    const restored = tab(persistent, a.session);
    await start(restored.auth);
    expect(restored.auth.getItem()).toBe(value('token'));
  });

  it('documents that a restored peer can authenticate a login page after refresh', async () => {
    vi.useFakeTimers();
    const persistent = new MemoryStorage();
    const original = tab(persistent);
    await start(original.auth);
    original.auth.setItem('', value('previous-browser-session'));
    const savedSecondTab = new MemoryStorage();
    savedSecondTab.values = new Map(original.session.values);
    original.auth.dispose();

    // The first restored page has no session; the second is restored later.
    const first = tab(persistent);
    await start(first.auth);
    expect(first.auth.getAccessToken()).toBeNull();
    const second = tab(persistent, savedSecondTab);
    await start(second.auth);
    expect(second.auth.getAccessToken()).toBe('previous-browser-session');
    expect(first.auth.getAccessToken()).toBeNull();

    first.auth.dispose();
    const refreshedFirst = tab(persistent, first.session);
    await start(refreshedFirst.auth);
    expect(refreshedFirst.auth.getAccessToken()).toBe('previous-browser-session');
  });
});
