import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { useNotificationStore, type NotificationKind } from '../stores/NotificationStore';
import { Toaster, TOAST_EXIT_MS } from './Toaster';

(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
  useNotificationStore.getState().clear();
  container = document.createElement('div');
  document.body.appendChild(container);
  root = createRoot(container);
});

afterEach(() => {
  act(() => root.unmount());
  container.remove();
  useNotificationStore.getState().clear();
});

const KINDS: NotificationKind[] = ['error', 'warning', 'success', 'info'];

describe('Toaster status glyphs', () => {
  // The point of the status icon is a non-colour severity channel (WCAG 1.4.1) — so
  // every kind must render a leading glyph, not lean on the border colour alone.
  test.each(KINDS)('a %s toast renders a leading status glyph', (kind) => {
    act(() => {
      useNotificationStore.getState().notify({ kind, title: `${kind} title`, message: 'body' });
      root.render(<Toaster />);
    });
    const toast = container.querySelector('[role="alert"], [role="status"]');
    expect(toast).not.toBeNull();
    // The status glyph is a direct-child <svg> of the toast (the close button's svg
    // is nested inside the button, so :scope > svg isolates the status icon).
    expect(toast!.querySelector(':scope > svg')).not.toBeNull();
  });

  test('error and warning are assertive; success and info are polite', () => {
    act(() => {
      useNotificationStore.getState().notify({ kind: 'error', title: 'e', message: 'b' });
      useNotificationStore.getState().notify({ kind: 'success', title: 's', message: 'b' });
      root.render(<Toaster />);
    });
    expect(container.querySelector('[role="alert"]')).not.toBeNull();
    expect(container.querySelector('[role="status"]')).not.toBeNull();
  });
});

describe('Toaster exit animation (two-phase dismiss)', () => {
  test('close button applies the exiting class immediately and keeps the toast visible', () => {
    vi.useFakeTimers();
    let id: string;
    act(() => {
      id = useNotificationStore.getState().notify({ kind: 'error', title: 'T', message: 'M' });
      root.render(<Toaster />);
    });

    // Toast is visible.
    const closeBtn = container.querySelector('button[aria-label="Dismiss notification"]');
    expect(closeBtn).not.toBeNull();

    // Click close — begins the exit phase.
    act(() => {
      closeBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    // Still in the DOM (store not yet cleared), but carrying the exiting class.
    const toast = container.querySelector('[role="alert"]');
    expect(toast).not.toBeNull();
    expect(toast!.className).toMatch(/exiting/);

    // Notification still in the store during the animation window.
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(true);

    vi.useRealTimers();
  });

  test('toast is removed from the store after TOAST_EXIT_MS', () => {
    vi.useFakeTimers();
    let id: string;
    act(() => {
      id = useNotificationStore.getState().notify({ kind: 'error', title: 'T', message: 'M' });
      root.render(<Toaster />);
    });

    const closeBtn = container.querySelector('button[aria-label="Dismiss notification"]');
    act(() => {
      closeBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    // Just before the timeout fires — still in the store.
    act(() => {
      vi.advanceTimersByTime(TOAST_EXIT_MS - 1);
    });
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(true);

    // After the timeout — removed from the store.
    act(() => {
      vi.advanceTimersByTime(1);
    });
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(false);

    vi.useRealTimers();
  });

  test('auto-dismiss (success) also plays the exit animation before removing', () => {
    vi.useFakeTimers();
    const AUTO_DISMISS_MS = 6000;
    let id: string;
    act(() => {
      id = useNotificationStore.getState().notify({ kind: 'success', title: 'T', message: 'M' });
      root.render(<Toaster />);
    });

    // Before auto-dismiss fires — toast is visible and not exiting.
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(true);
    const toastBefore = container.querySelector('[role="status"]');
    expect(toastBefore).not.toBeNull();
    expect(toastBefore!.className).not.toMatch(/exiting/);

    // At the auto-dismiss threshold — exit phase begins.
    act(() => {
      vi.advanceTimersByTime(AUTO_DISMISS_MS);
    });
    const toastMid = container.querySelector('[role="status"]');
    expect(toastMid).not.toBeNull();
    expect(toastMid!.className).toMatch(/exiting/);
    // Still in store during animation.
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(true);

    // After the exit animation — removed from store.
    act(() => {
      vi.advanceTimersByTime(TOAST_EXIT_MS);
    });
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(false);

    vi.useRealTimers();
  });

  test('multiple toasts can be in the exiting state simultaneously', () => {
    vi.useFakeTimers();
    act(() => {
      useNotificationStore.getState().notify({ kind: 'error', title: 'A', message: 'a' });
      useNotificationStore.getState().notify({ kind: 'warning', title: 'B', message: 'b' });
      root.render(<Toaster />);
    });

    // Click close on both toasts.
    const closeBtns = container.querySelectorAll('button[aria-label="Dismiss notification"]');
    expect(closeBtns.length).toBe(2);
    act(() => {
      closeBtns[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));
      closeBtns[1].dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    // Both are in the exiting state — both still in the DOM.
    const toasts = container.querySelectorAll('[role="alert"]');
    expect(toasts.length).toBe(2);
    toasts.forEach((t) => expect(t.className).toMatch(/exiting/));

    // Both removed from the store after the timeout.
    act(() => {
      vi.advanceTimersByTime(TOAST_EXIT_MS);
    });
    expect(useNotificationStore.getState().notifications).toHaveLength(0);

    vi.useRealTimers();
  });

  test('a durable toast does not auto-dismiss, and its action runs then dismisses it', () => {
    vi.useFakeTimers();
    const onClick = vi.fn();
    let id: string;
    act(() => {
      id = useNotificationStore.getState().notify({
        kind: 'info',
        title: 'Update available',
        message: 'A new version is ready.',
        durable: true,
        action: { label: 'Reload', onClick },
      });
      root.render(<Toaster />);
    });

    // Well past the auto-dismiss window — still present (durable).
    act(() => {
      vi.advanceTimersByTime(60000);
    });
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(true);

    // The action button runs the handler, then the toast exits.
    const actionBtn = Array.from(container.querySelectorAll('button')).find(
      (b) => b.textContent === 'Reload',
    );
    expect(actionBtn).toBeTruthy();
    act(() => {
      actionBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });
    expect(onClick).toHaveBeenCalledTimes(1);

    act(() => {
      vi.advanceTimersByTime(TOAST_EXIT_MS);
    });
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(false);

    vi.useRealTimers();
  });

  test('clicking close twice does not reset the exit timer', () => {
    vi.useFakeTimers();
    let id: string;
    act(() => {
      id = useNotificationStore.getState().notify({ kind: 'error', title: 'T', message: 'M' });
      root.render(<Toaster />);
    });

    const closeBtn = container.querySelector('button[aria-label="Dismiss notification"]');
    act(() => {
      closeBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });
    // A second click while already exiting — should be a no-op for the timer.
    act(() => {
      closeBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    act(() => {
      vi.advanceTimersByTime(TOAST_EXIT_MS);
    });
    // Notification is gone — second click didn't schedule a duplicate removal.
    expect(useNotificationStore.getState().notifications.some((n) => n.id === id!)).toBe(false);

    vi.useRealTimers();
  });
});
