import { describe, it, expect } from '#test';
import { localStorageBackedSession, LocalStorageSession, __sessionMetadataCache } from './session.ts';


const uuidv4 = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
const uuidStr = expect.stringMatching(uuidv4);

const suite = describe({
  name: 'localStorageBackedSession()',
  beforeEach() {
    localStorage.clear();
    __sessionMetadataCache.clear();
  }
});
const jsonSuite = describe({
  suite,
  name: 'JSON parsing'
});

it(suite, 'should have empty cache and localStorage before reading session', () => {
  localStorageBackedSession() satisfies LocalStorageSession;
  expect(localStorage.length).toBe(0);
  expect(__sessionMetadataCache.size).toBe(0);
});

it(suite, 'should return randomized uuid keys for empty storage', () => {
  const session = localStorageBackedSession();

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('voyado.session')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(suite, 'should return the same cached value when retrieved multiple times', () => {
  const session = localStorageBackedSession();

  const data = session();
  localStorage.clear();

  expect(data).toBe(session());
  expect(data).toBe(session());
});

it(suite, 'should return new random keys when cache and localStorage is cleared', () => {
  const session = localStorageBackedSession();

  const data = session();
  localStorage.clear();
  __sessionMetadataCache.clear();

  expect(data).not.toEqual(session());
});

it(suite, 'should clear cache key when window storage event occurs', () => {
  const session = localStorageBackedSession('storageEv');

  const data = session();
  const sizePriorDispatch = __sessionMetadataCache.size;
  globalThis.dispatchEvent(new StorageEvent('storage', {
    key: 'storageEv',
    storageArea: localStorage
  }));

  expect(sizePriorDispatch).toBe(1);
  expect(__sessionMetadataCache.size).toBe(0);
  expect(data).not.toBe(session());
  expect(data).toEqual(session());
});

it(suite, 'should NOT clear cache key when window storage event does not match', () => {
  const session = localStorageBackedSession('nonStorageEv');

  const data = session();
  const sizePriorDispatch = __sessionMetadataCache.size;
  globalThis.dispatchEvent(new StorageEvent('storage', {
    key: '=== Non-match ===',
    storageArea: localStorage
  }));
  globalThis.dispatchEvent(new StorageEvent('storage', {
    key: 'nonStorageEv',
    storageArea: sessionStorage
  }));
  globalThis.dispatchEvent(new Event('click'));

  expect(sizePriorDispatch).toBe(1);
  expect(__sessionMetadataCache.size).toBe(1);
  expect(data).toBe(session());
});

it(suite, 'should return new randomized keys when clear is run', () => {
  const session = localStorageBackedSession('reset');

  const a = session();
  session.reset();
  const b = session();

  expect(a).not.toEqual(b);
  expect(b).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('reset')).toBe(`{"customerKey":"${b.customerKey}","sessionKey":"${b.sessionKey}"}`);
});

it(suite, 'should return the latest set customerKey', () => {
  const session = localStorageBackedSession('update');

  session.updateCustomerKey('Bruce Wayne 🦇');
  const result = session();

  expect(result).toEqual({ customerKey: 'Bruce Wayne 🦇', sessionKey: uuidStr });
  expect(localStorage.getItem('update')).toBe(`{"customerKey":"Bruce Wayne 🦇","sessionKey":"${result.sessionKey}"}`);
});


it(jsonSuite, 'should return existing storage keys', () => {
  const session = localStorageBackedSession('existing');
  localStorage.setItem('existing', '{"customerKey":"foo","sessionKey":"bar"}');

  const result = session();

  expect(result).toEqual({ customerKey: 'foo', sessionKey: 'bar' });
  expect(localStorage.getItem('existing')).toBe('{"customerKey":"foo","sessionKey":"bar"}');
});

it(jsonSuite, 'should return randomized uuid keys for invalid JSON', () => {
  const session = localStorageBackedSession('invalid');
  localStorage.setItem('invalid', 'not actually JSON! 😲');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('invalid')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should reset data for stored boolean JSON data', () => {
  const session = localStorageBackedSession('bool');
  localStorage.setItem('bool', 'true');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('bool')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should reset data for stored number JSON data', () => {
  const session = localStorageBackedSession('num');
  localStorage.setItem('num', '987.123');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('num')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should reset data for stored string JSON data', () => {
  const session = localStorageBackedSession('str');
  localStorage.setItem('str', '"Hello there!"');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('str')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should reset data for stored null JSON data', () => {
  const session = localStorageBackedSession('nully');
  localStorage.setItem('nully', 'null');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('nully')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should reset data for stored array JSON data', () => {
  const session = localStorageBackedSession('arr');
  localStorage.setItem('arr', '["foo", "bar"]');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('arr')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should repair broken stored empty metadata on read', () => {
  const session = localStorageBackedSession('empty');
  localStorage.setItem('empty', '{}');

  const result = session();
  const { customerKey, sessionKey } = result;

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: uuidStr });
  expect(localStorage.getItem('empty')).toBe(`{"customerKey":"${customerKey}","sessionKey":"${sessionKey}"}`);
});

it(jsonSuite, 'should repair broken stored partial metadata on read', () => {
  const session = localStorageBackedSession('repair');
  localStorage.setItem('repair', '{"customerKey":"partial"}');

  const result = session();

  expect(result).toEqual({ customerKey: 'partial', sessionKey: uuidStr });
  expect(localStorage.getItem('repair')).toBe(`{"customerKey":"partial","sessionKey":"${result.sessionKey}"}`);
});

it(jsonSuite, 'should repair invalid primitive type on "sessionKey"', () => {
  const session = localStorageBackedSession('s-type');
  localStorage.setItem('s-type', '{"customerKey":"abc","sessionKey":99}');

  const result = session();

  expect(result).toEqual({ customerKey: 'abc', sessionKey: uuidStr });
  expect(localStorage.getItem('s-type')).toBe(`{"customerKey":"abc","sessionKey":"${result.sessionKey}"}`);
});

it(jsonSuite, 'should repair invalid object type on "sessionKey"', () => {
  const session = localStorageBackedSession('s-type');
  localStorage.setItem('s-type', '{"customerKey":"abc","sessionKey":{}}');

  const result = session();

  expect(result).toEqual({ customerKey: 'abc', sessionKey: uuidStr });
  expect(localStorage.getItem('s-type')).toBe(`{"customerKey":"abc","sessionKey":"${result.sessionKey}"}`);
});

it(jsonSuite, 'should repair invalid primitive type on "customerKey"', () => {
  const session = localStorageBackedSession('c-prim');
  localStorage.setItem('c-prim', '{"customerKey":true,"sessionKey":"42"}');

  const result = session();

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: '42' });
  expect(localStorage.getItem('c-prim')).toBe(`{"customerKey":"${result.customerKey}","sessionKey":"42"}`);
});

it(jsonSuite, 'should repair invalid object type on "customerKey"', () => {
  const session = localStorageBackedSession('c-obj');
  localStorage.setItem('c-obj', '{"customerKey":{},"sessionKey":"42"}');

  const result = session();

  expect(result).toEqual({ customerKey: uuidStr, sessionKey: '42' });
  expect(localStorage.getItem('c-obj')).toBe(`{"customerKey":"${result.customerKey}","sessionKey":"42"}`);
});

it(jsonSuite, 'should discard extraneous properties on stored metadata on read', () => {
  const session = localStorageBackedSession('extraneous');
  localStorage.setItem('extraneous', '{"foo":"foo","customerKey":"123","sessionKey":"321","bar":"bar"}');

  const result = session();

  expect(result).toEqual({ customerKey: '123', sessionKey: '321' });
  expect(localStorage.getItem('extraneous')).toBe(`{"customerKey":"123","sessionKey":"321"}`);
});


class StorageEvent extends Event {
  readonly key: string;
  readonly storageArea: Storage;

  constructor(type: string, eventInitDict: StorageEventInit) {
    super(type, eventInitDict);
    this.key = eventInitDict.key!;
    this.storageArea = eventInitDict.storageArea!;
  }
}
