/**
 * @fileoverview URL Trigger Handler Test (P4-03)
 * @description
 * Tests for:
 * - URL matching semantics (domain, path, url prefix)
 * - Listener lifecycle (add/remove on install/uninstall)
 * - Edge cases (subframe, invalid URL)
 */

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

import type { TriggerSpecByKind } from '@/entrypoints/background/record-replay-v3/domain/triggers';
import type { TriggerFireCallback } from '@/entrypoints/background/record-replay-v3/engine/triggers/trigger-handler';
import { createUrlTriggerHandlerFactory } from '@/entrypoints/background/record-replay-v3/engine/triggers/url-trigger';

// ==================== Test Utilities ====================

function createSilentLogger(): Pick<Console, 'debug' | 'info' | 'warn' | 'error'> {
  return {
    debug: () => {},
    info: () => {},
    warn: () => {},
    error: () => {},
  };
}

interface WebNavigationMock {
  onCompleted: {
    addListener: ReturnType<typeof vi.fn>;
    removeListener: ReturnType<typeof vi.fn>;
  };
  emitCompleted: (details: { tabId: number; frameId: number; url: string }) => void;
}

function createWebNavigationMock(): WebNavigationMock {
  const listeners = new Set<(details: unknown) => void>();

  const onCompleted = {
    addListener: vi.fn((cb: (details: unknown) => void) => {
      listeners.add(cb);
    }),
    removeListener: vi.fn((cb: (details: unknown) => void) => {
      listeners.delete(cb);
    }),
  };

  return {
    onCompleted,
    emitCompleted: (details) => {
      for (const cb of listeners) cb(details);
    },
  };
}

// ==================== URL Trigger Tests ====================

describe('V3 UrlTriggerHandler', () => {
  let webNav: WebNavigationMock;

  beforeEach(() => {
    webNav = createWebNavigationMock();
    (globalThis.chrome as unknown as { webNavigation: unknown }).webNavigation = {
      onCompleted: webNav.onCompleted,
    };
  });

  describe('Domain matching', () => {
    it('matches exact domain', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'example.com' }],
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com/page' });
      expect(fireCallback.onFire).toHaveBeenCalledWith('t1', {
        sourceTabId: 1,
        sourceUrl: 'https://example.com/page',
      });
    });

    it('matches subdomain', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'example.com' }],
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://www.example.com/a' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(1);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://sub.sub.example.com/b' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(2);
    });

    it('avoids substring false-positives', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'example.com' }],
      };

      await handler.install(trigger);

      // Should NOT match - domain contains "example.com" as substring but is not example.com or subdomain
      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://notexample.com/a' });
      expect(fireCallback.onFire).not.toHaveBeenCalled();

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com.evil.com/a' });
      expect(fireCallback.onFire).not.toHaveBeenCalled();
    });

    it('handles domain with leading/trailing dots', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: '..example.com..' }],
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com/page' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(1);
    });
  });

  describe('Path matching', () => {
    it('matches path prefix', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'path', value: '/foo' }],
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://any.com/foo/bar' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(1);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://any.com/foobar' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(2);
    });

    it('does not match non-matching path', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'path', value: '/foo' }],
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://any.com/bar' });
      expect(fireCallback.onFire).not.toHaveBeenCalled();
    });

    it('normalizes path without leading slash', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'path', value: 'foo' }], // No leading slash
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://any.com/foo/bar' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(1);
    });
  });

  describe('URL prefix matching', () => {
    it('matches full URL prefix', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'url', value: 'https://example.com/a' }],
      };

      await handler.install(trigger);

      // Matches prefix with query/hash
      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com/a?x=1#hash' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(1);

      // Matches prefix with additional path
      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com/a/b/c' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(2);
    });

    it('does not match non-matching URL', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'url', value: 'https://example.com/a' }],
      };

      await handler.install(trigger);

      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com/b' });
      expect(fireCallback.onFire).not.toHaveBeenCalled();
    });
  });

  describe('Multiple rules (OR logic)', () => {
    it('fires if any rule matches', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [
          { kind: 'domain', value: 'example.com' },
          { kind: 'path', value: '/special' },
        ],
      };

      await handler.install(trigger);

      // Match by domain
      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://example.com/any' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(1);

      // Match by path on different domain
      webNav.emitCompleted({ tabId: 1, frameId: 0, url: 'https://other.com/special/page' });
      expect(fireCallback.onFire).toHaveBeenCalledTimes(2);
    });
  });

  describe('Frame filtering', () => {
    it('ignores subframe navigations', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'example.com' }],
      };

      await handler.install(trigger);

      // frameId !== 0 should be ignored
      webNav.emitCompleted({ tabId: 1, frameId: 1, url: 'https://example.com/' });
      expect(fireCallback.onFire).not.toHaveBeenCalled();

      webNav.emitCompleted({ tabId: 1, frameId: 99, url: 'https://example.com/' });
      expect(fireCallback.onFire).not.toHaveBeenCalled();
    });
  });

  describe('Listener lifecycle', () => {
    it('registers single listener on first install', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'a.com' }],
      };

      const t2: TriggerSpecByKind<'url'> = {
        id: 't2' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'b.com' }],
      };

      await handler.install(t1);
      await handler.install(t2);

      // Only one listener should be added
      expect(webNav.onCompleted.addListener).toHaveBeenCalledTimes(1);
    });

    it('removes listener when all triggers uninstalled', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'a.com' }],
      };

      const t2: TriggerSpecByKind<'url'> = {
        id: 't2' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'b.com' }],
      };

      await handler.install(t1);
      await handler.install(t2);

      await handler.uninstall('t1');
      expect(webNav.onCompleted.removeListener).not.toHaveBeenCalled();

      await handler.uninstall('t2');
      expect(webNav.onCompleted.removeListener).toHaveBeenCalledTimes(1);
    });

    it('removes listener on uninstallAll', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'example.com' }],
      };

      await handler.install(t1);
      await handler.uninstallAll();

      expect(webNav.onCompleted.removeListener).toHaveBeenCalledTimes(1);
    });
  });

  describe('getInstalledIds', () => {
    it('returns installed trigger IDs', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createUrlTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'url'> = {
        id: 't1' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'a.com' }],
      };

      const t2: TriggerSpecByKind<'url'> = {
        id: 't2' as never,
        kind: 'url',
        enabled: true,
        flowId: 'flow-1' as never,
        match: [{ kind: 'domain', value: 'b.com' }],
      };

      await handler.install(t1);
      await handler.install(t2);

      expect(handler.getInstalledIds().sort()).toEqual(['t1', 't2']);

      await handler.uninstall('t1');
      expect(handler.getInstalledIds()).toEqual(['t2']);
    });
  });
});
