/**
 * notify.ts — route_to_user unit tests
 *
 * Critical behavioral assertion (D-12 / ROUTE-05):
 * When a user has the (event_key, channel_type) toggle DISABLED in
 * user_event_subscriptions, the notify step must return success:true with
 * notified:false and MUST NOT contact the global channel or any personal
 * channel. Grep cannot prove this — only a behavioral test can.
 */

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

// Mock postgresClient BEFORE importing the module under test.
const queryMock = vi.fn();
vi.mock('../postgres-client', () => ({
  postgresClient: {
    query: (...args: unknown[]) => queryMock(...args),
  },
}));

// Mock the resolver registry to short-circuit user resolution.
vi.mock('./notify-resolvers', () => ({
  resolveRecipient: vi.fn(async () => ({
    recipient: { user_id: 'test-user-id' },
    resolverFound: true,
  })),
  RESOLVERS: new Map(),
  registerResolver: vi.fn(),
}));

// Mock pipeline-engine to prevent the registerStepExecutor side effect.
vi.mock('../pipeline-engine', () => ({
  registerStepExecutor: vi.fn(),
}));

// Mock global fetch so any stray send call is observable and doesn't hit network.
const fetchMock = vi.fn(async () => new Response('ok', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);

// Import AFTER mocks are declared so vi.mock hoisting takes effect.
import { _INTERNALS } from './notify';

describe('notify.ts route_to_user', () => {
  beforeEach(() => {
    queryMock.mockReset();
    fetchMock.mockReset();
    fetchMock.mockResolvedValue(new Response('ok', { status: 200 }));
  });

  it('muted user must not fall back to global channel', async () => {
    // Arrange: user has the (event_key, channel_type) toggle DISABLED.
    // Any SQL beyond the user_event_subscriptions check is a failure —
    // the mute branch must short-circuit before querying notification_channels
    // or calling fallbackToGlobal.
    queryMock.mockImplementation((sql: string) => {
      if (sql.includes('user_event_subscriptions')) {
        return Promise.resolve({ rows: [{ enabled: false }], rowCount: 1 });
      }
      // Any other SQL (notification_channels, "user" lookup, etc.) is unexpected.
      throw new Error(`Unexpected SQL after mute: ${String(sql).slice(0, 80)}`);
    });

    const step = {
      id: 1,
      step_type: 'notify' as const,
      name: 'test',
      config: {
        channel_id: 99,
        message: 'hi',
        route_to_user: {
          source: 'ticket',
          field: 'assignedResourceID',
          resolve: 'pulse_user_id',
          event_key: 'ticket_assigned_to_me',
          channel_type: 'ntfy',
        },
      },
    };
    const context = { ticket: { assignedResourceID: 'test-user-id' } };

    // Act — call dispatchUserRoute directly via the _INTERNALS test seam.
    const result = await _INTERNALS.dispatchUserRoute({
      step: step as any,
      context,
      message: 'hi',
      channelId: 99,
      route: step.config.route_to_user as any,
    });

    // Assert: success + notified=false + skipped_reason=user_muted, NO fallback key.
    expect(result.success).toBe(true);
    expect(result.output?.notified).toBe(false);
    expect(result.output?.skipped_reason).toBe('user_muted');
    expect(result.output?.user_route_fallback).toBeUndefined();

    // The notification_channels SELECT must NEVER have been issued.
    const calls = queryMock.mock.calls.map((c) => String(c[0]));
    expect(calls.some((sql) => sql.includes('notification_channels'))).toBe(false);

    // No outbound HTTP request must have occurred (no send to personal or global channel).
    expect(fetchMock).not.toHaveBeenCalled();
  });
});
