import { AxiosError } from 'axios';

import { AppInitializer } from './AppInitializer';

const networkError = () => {
  const e = new AxiosError('Network Error');
  e.response = undefined;
  return e;
};

const authError = () => {
  const e = new AxiosError('Unauthorized');
  (e as unknown as { response: unknown }).response = {
    status: 401,
    data: {},
  };
  return e;
};

const makeOptions = (
  overrides: Partial<Parameters<AppInitializer['run']>[1]> = {},
) => ({
  hasValidTokens: () => true,
  onAuthFailure: jest.fn(),
  ...overrides,
});

describe('AppInitializer', () => {
  beforeEach(() => jest.useFakeTimers());
  afterEach(() => jest.useRealTimers());

  describe('normal boot', () => {
    it('calls bootFn once and resolves on success', async () => {
      const initializer = new AppInitializer();
      const bootFn = jest.fn().mockResolvedValue(undefined);
      const options = makeOptions();

      await initializer.run(bootFn, options);

      expect(bootFn).toHaveBeenCalledTimes(1);
      expect(options.onAuthFailure).not.toHaveBeenCalled();
    });
  });

  describe('network error retry', () => {
    it('retries on network error and succeeds on second attempt', async () => {
      const initializer = new AppInitializer();
      const bootFn = jest
        .fn()
        .mockRejectedValueOnce(networkError())
        .mockResolvedValue(undefined);
      const options = makeOptions();

      const runPromise = initializer.run(bootFn, options);
      await Promise.resolve();
      await runPromise;

      expect(bootFn).toHaveBeenCalledTimes(2);
      expect(options.onAuthFailure).not.toHaveBeenCalled();
    });

    it('calls onAuthFailure on network error when no valid tokens', async () => {
      const initializer = new AppInitializer();
      const bootFn = jest.fn().mockRejectedValue(networkError());
      const options = makeOptions({ hasValidTokens: () => false });

      await initializer.run(bootFn, options);

      expect(options.onAuthFailure).toHaveBeenCalledTimes(1);
      expect(bootFn).toHaveBeenCalledTimes(1);
    });
  });

  describe('auth error', () => {
    it('calls onAuthFailure immediately on auth error without retrying', async () => {
      const initializer = new AppInitializer();
      const bootFn = jest.fn().mockRejectedValue(authError());
      const options = makeOptions();

      await initializer.run(bootFn, options);

      expect(options.onAuthFailure).toHaveBeenCalledTimes(1);
      expect(bootFn).toHaveBeenCalledTimes(1);
    });
  });

  describe('abort during backoff (abortable wait)', () => {
    it('exits loop promptly when aborted mid-wait, not after full timeout', async () => {
      const initializer = new AppInitializer();
      const bootFn = jest.fn().mockRejectedValue(networkError());
      const options = makeOptions();

      const runPromise = initializer.run(bootFn, options);

      await Promise.resolve();
      await Promise.resolve();

      initializer.abort();

      await expect(runPromise).resolves.toBeUndefined();
      expect(jest.getTimerCount()).toBe(0);
    });
  });

  describe('StrictMode race condition', () => {
    it('second run() waits for aborted first run and starts fresh', async () => {
      const initializer = new AppInitializer();

      const callOrder: string[] = [];
      const bootFn1 = jest.fn().mockImplementation(async () => {
        callOrder.push('bootFn1');
      });
      const bootFn2 = jest.fn().mockImplementation(async () => {
        callOrder.push('bootFn2');
      });

      const options1 = makeOptions();
      const options2 = makeOptions();

      const run1 = initializer.run(bootFn1, options1);
      initializer.abort();
      const run2 = initializer.run(bootFn2, options2);

      await Promise.all([run1, run2]);

      expect(bootFn2).toHaveBeenCalledTimes(1);
      expect(options2.onAuthFailure).not.toHaveBeenCalled();
    });

    it("second run() uses its own bootFn and options, not the first run's", async () => {
      const initializer = new AppInitializer();

      const bootFn1 = jest.fn().mockRejectedValue(networkError());
      const bootFn2 = jest.fn().mockResolvedValue(undefined);
      const options1 = makeOptions();
      const options2 = makeOptions();

      const run1 = initializer.run(bootFn1, options1);
      initializer.abort();
      const run2 = initializer.run(bootFn2, options2);

      await Promise.all([run1, run2]);

      expect(bootFn2).toHaveBeenCalledTimes(1);
      expect(options1.onAuthFailure).not.toHaveBeenCalled();
      expect(options2.onAuthFailure).not.toHaveBeenCalled();
    });
  });

  describe('runningPromise cleanup does not clobber a newer run()', () => {
    it('Mount 1 cleanup does not null out Mount 2 in-flight promise', async () => {
      const initializer = new AppInitializer();

      const bootFn1 = jest.fn().mockResolvedValue(undefined);
      const bootFn2 = jest.fn().mockResolvedValue(undefined);
      const options1 = makeOptions();
      const options2 = makeOptions();

      const run1 = initializer.run(bootFn1, options1);
      initializer.abort();
      const run2 = initializer.run(bootFn2, options2);

      await Promise.all([run1, run2]);

      expect(bootFn2).toHaveBeenCalledTimes(1);
      expect(options1.onAuthFailure).not.toHaveBeenCalled();
      expect(options2.onAuthFailure).not.toHaveBeenCalled();

      const bootFn3 = jest.fn().mockResolvedValue(undefined);
      const options3 = makeOptions();
      await initializer.run(bootFn3, options3);
      expect(bootFn3).toHaveBeenCalledTimes(1);
    });
  });

  describe('onAuthFailure fires only once', () => {
    it('does not call onAuthFailure twice in StrictMode remount when first run hits auth error', async () => {
      const initializer = new AppInitializer();

      const bootFn1 = jest.fn().mockRejectedValue(authError());
      const bootFn2 = jest.fn().mockRejectedValue(authError());
      const onAuthFailure = jest.fn();
      const options1 = makeOptions({ onAuthFailure });
      const options2 = makeOptions({ onAuthFailure });

      const run1 = initializer.run(bootFn1, options1);
      initializer.abort();
      const run2 = initializer.run(bootFn2, options2);

      await Promise.all([run1, run2]);

      expect(onAuthFailure).toHaveBeenCalledTimes(1);
    });
  });
});
