import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  mapStatusToEvent,
  shouldNotify,
  buildNotificationOptions,
  NotificationDebouncer,
  DEFAULT_NOTIFY_EVENTS,
  createNotification,
  isNotificationSupported,
  getNotificationPermission,
  type NotifyEventSettings,
} from './notifications';

describe('mapStatusToEvent', () => {
  it('maps known job statuses to notify event types', () => {
    expect(mapStatusToEvent('running')).toBe('running');
    expect(mapStatusToEvent('succeeded')).toBe('succeeded');
    expect(mapStatusToEvent('failed')).toBe('failed');
    expect(mapStatusToEvent('aborted')).toBe('failed');
    expect(mapStatusToEvent('waiting_human')).toBe('waiting_human');
  });

  it('returns null for non-notify statuses', () => {
    expect(mapStatusToEvent('queued')).toBeNull();
    expect(mapStatusToEvent('dispatching')).toBeNull();
    expect(mapStatusToEvent('waiting_subtasks')).toBeNull();
    expect(mapStatusToEvent('')).toBeNull();
    expect(mapStatusToEvent(undefined as unknown as string)).toBeNull();
  });
});

describe('shouldNotify', () => {
  const allOn: NotifyEventSettings = {
    running: true,
    succeeded: true,
    failed: true,
    waiting_human: true,
  };

  it('returns event type when transition is enabled', () => {
    expect(shouldNotify('queued', 'running', allOn)).toBe('running');
    expect(shouldNotify('running', 'succeeded', allOn)).toBe('succeeded');
    expect(shouldNotify('running', 'failed', allOn)).toBe('failed');
    expect(shouldNotify('running', 'aborted', allOn)).toBe('failed');
    expect(shouldNotify('running', 'waiting_human', allOn)).toBe('waiting_human');
  });

  it('returns null when prev equals curr (no transition)', () => {
    expect(shouldNotify('running', 'running', allOn)).toBeNull();
  });

  it('returns null when prev is undefined (initial snapshot)', () => {
    expect(shouldNotify(undefined, 'succeeded', allOn)).toBeNull();
  });

  it('returns null when event is disabled in settings', () => {
    const settings: NotifyEventSettings = { ...allOn, succeeded: false };
    expect(shouldNotify('running', 'succeeded', settings)).toBeNull();
  });

  it('returns null for non-notify statuses', () => {
    expect(shouldNotify('running', 'queued', allOn)).toBeNull();
    expect(shouldNotify('running', 'waiting_subtasks', allOn)).toBeNull();
  });
});

describe('buildNotificationOptions', () => {
  const task = { id: 42, title: 'Slack 投稿レビュー', pieceName: 'chat' };

  it('builds title with emoji and label for succeeded', () => {
    const opts = buildNotificationOptions(task, 'succeeded');
    expect(opts.title).toBe('✅ Task completed: Slack 投稿レビュー');
    expect(opts.body).toBe('chat');
    expect(opts.tag).toBe('task-42-succeeded');
    // Vite BASE_URL prefix — '/' in test/dev, '/ui/' in production build
    expect(opts.icon).toBe(`${import.meta.env.BASE_URL}favicon.svg`);
  });

  it('uses correct emoji for each event type', () => {
    expect(buildNotificationOptions(task, 'running').title).toContain('🟢');
    expect(buildNotificationOptions(task, 'failed').title).toContain('❌');
    expect(buildNotificationOptions(task, 'waiting_human').title).toContain('❓');
  });
});

describe('NotificationDebouncer', () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { vi.useRealTimers(); });

  it('fires callback after debounceMs with final status', () => {
    const fired: Array<{ taskId: number; status: string }> = [];
    const debouncer = new NotificationDebouncer(
      4000,
      (taskId, status) => fired.push({ taskId, status }),
    );

    debouncer.schedule(1, 'succeeded');
    expect(fired).toEqual([]);  // not yet
    vi.advanceTimersByTime(4000);
    expect(fired).toEqual([{ taskId: 1, status: 'succeeded' }]);
  });

  it('replaces pending status when scheduled again within window', () => {
    const fired: Array<{ taskId: number; status: string }> = [];
    const debouncer = new NotificationDebouncer(
      4000,
      (taskId, status) => fired.push({ taskId, status }),
    );

    debouncer.schedule(1, 'failed');
    vi.advanceTimersByTime(2000);
    debouncer.schedule(1, 'running');  // retry started
    vi.advanceTimersByTime(2000);
    debouncer.schedule(1, 'succeeded');  // final
    vi.advanceTimersByTime(4000);

    // Only the final status fires, not the intermediate failed.
    expect(fired).toEqual([{ taskId: 1, status: 'succeeded' }]);
  });

  it('cancels pending callback', () => {
    const fired: Array<{ taskId: number; status: string }> = [];
    const debouncer = new NotificationDebouncer(
      4000,
      (taskId, status) => fired.push({ taskId, status }),
    );

    debouncer.schedule(1, 'succeeded');
    debouncer.cancel(1);
    vi.advanceTimersByTime(4000);
    expect(fired).toEqual([]);
  });

  it('handles multiple tasks independently', () => {
    const fired: Array<{ taskId: number; status: string }> = [];
    const debouncer = new NotificationDebouncer(
      4000,
      (taskId, status) => fired.push({ taskId, status }),
    );

    debouncer.schedule(1, 'succeeded');
    debouncer.schedule(2, 'failed');
    vi.advanceTimersByTime(4000);
    expect(fired).toEqual([
      { taskId: 1, status: 'succeeded' },
      { taskId: 2, status: 'failed' },
    ]);
  });

  it('clearAll cancels all pending timers', () => {
    const fired: Array<{ taskId: number; status: string }> = [];
    const debouncer = new NotificationDebouncer(
      4000,
      (taskId, status) => fired.push({ taskId, status }),
    );

    debouncer.schedule(1, 'succeeded');
    debouncer.schedule(2, 'failed');
    debouncer.clearAll();
    vi.advanceTimersByTime(4000);
    expect(fired).toEqual([]);
  });
});

describe('DEFAULT_NOTIFY_EVENTS', () => {
  it('has all four event types enabled by default', () => {
    expect(DEFAULT_NOTIFY_EVENTS).toEqual({
      running: true,
      succeeded: true,
      failed: true,
      waiting_human: true,
    });
  });
});

describe('createNotification', () => {
  let originalNotification: typeof Notification | undefined;
  beforeEach(() => {
    originalNotification = (globalThis as any).Notification;
  });
  afterEach(() => {
    (globalThis as any).Notification = originalNotification;
  });

  it('returns null when Notification API is unavailable', () => {
    delete (globalThis as any).Notification;
    const result = createNotification(
      { title: 't', body: 'b', tag: 'x', icon: '/i', data: { taskId: 1 } },
      () => {},
    );
    expect(result).toBeNull();
  });

  it('returns null when permission is not granted', () => {
    const mockCtor = vi.fn();
    (globalThis as any).Notification = Object.assign(mockCtor, { permission: 'denied' });
    const result = createNotification(
      { title: 't', body: 'b', tag: 'x', icon: '/i', data: { taskId: 1 } },
      () => {},
    );
    expect(result).toBeNull();
    expect(mockCtor).not.toHaveBeenCalled();
  });

  it('constructs Notification when permission granted and attaches onclick', () => {
    const onClick = vi.fn();
    const onclickHandlers: Array<(e: Event) => void> = [];
    const mockCtor = vi.fn().mockImplementation(function (this: any, title: string, opts: any) {
      this.title = title;
      this.opts = opts;
      this.data = opts.data;
      Object.defineProperty(this, 'onclick', {
        set: (h: (e: Event) => void) => onclickHandlers.push(h),
      });
    });
    (globalThis as any).Notification = Object.assign(mockCtor, { permission: 'granted' });

    const result = createNotification(
      { title: 'T', body: 'B', tag: 'tag-1', icon: '/i', data: { taskId: 7 } },
      onClick,
    );
    expect(result).not.toBeNull();
    expect(mockCtor).toHaveBeenCalledWith('T', expect.objectContaining({
      body: 'B', tag: 'tag-1', icon: '/i', data: { taskId: 7 },
    }));
    // simulate click
    const fakeEvent = { preventDefault: vi.fn() } as unknown as Event;
    onclickHandlers[0](fakeEvent);
    expect(onClick).toHaveBeenCalledWith(7);
  });

  it('returns null and does not throw when constructor throws', () => {
    (globalThis as any).Notification = Object.assign(
      vi.fn(() => { throw new TypeError('blocked'); }),
      { permission: 'granted' },
    );
    const result = createNotification(
      { title: 't', body: 'b', tag: 'x', icon: '/i', data: { taskId: 1 } },
      () => {},
    );
    expect(result).toBeNull();
  });

  it("returns null when permission is 'default' (not yet granted)", () => {
    const mockCtor = vi.fn();
    (globalThis as any).Notification = Object.assign(mockCtor, { permission: 'default' });
    const result = createNotification(
      { title: 't', body: 'b', tag: 'x', icon: '/i', data: { taskId: 1 } },
      () => {},
    );
    expect(result).toBeNull();
    expect(mockCtor).not.toHaveBeenCalled();
  });
});

describe('isNotificationSupported', () => {
  let originalNotification: typeof Notification | undefined;
  beforeEach(() => {
    originalNotification = (globalThis as any).Notification;
  });
  afterEach(() => {
    (globalThis as any).Notification = originalNotification;
  });

  it('returns true when Notification exists', () => {
    (globalThis as any).Notification = Object.assign(vi.fn(), { permission: 'default' });
    expect(isNotificationSupported()).toBe(true);
  });

  it('returns false when Notification is undefined', () => {
    delete (globalThis as any).Notification;
    expect(isNotificationSupported()).toBe(false);
  });
});

describe('getNotificationPermission', () => {
  let originalNotification: typeof Notification | undefined;
  beforeEach(() => { originalNotification = (globalThis as any).Notification; });
  afterEach(() => { (globalThis as any).Notification = originalNotification; });

  it("returns 'unsupported' when Notification is unavailable", () => {
    delete (globalThis as any).Notification;
    expect(getNotificationPermission()).toBe('unsupported');
  });

  it('returns the current permission when supported', () => {
    (globalThis as any).Notification = Object.assign(vi.fn(), { permission: 'granted' });
    expect(getNotificationPermission()).toBe('granted');
    (globalThis as any).Notification = Object.assign(vi.fn(), { permission: 'denied' });
    expect(getNotificationPermission()).toBe('denied');
    (globalThis as any).Notification = Object.assign(vi.fn(), { permission: 'default' });
    expect(getNotificationPermission()).toBe('default');
  });
});
