import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import type { Channel, WhatsAppProvider } from '@wa-ticketing/shared';
import { getWhatsAppCapabilities } from './whatsappCapabilities.js';
import { WhatsAppProviderError } from './africasTalkingWhatsAppProvider.js';
import {
  assertCapability,
  canStaffSendOnChannel,
  isUnsupportedCapabilityError,
  redactInboundToken,
  serializeEmbeddedChannel,
  planChannelCreate,
  resolveCreateProvider,
  resolveWhatsAppTarget,
} from './channelProviderRules.js';

function channel(provider: WhatsAppProvider): Channel {
  return {
    id: 'ch-1', name: 'Support', phone_number: '+27820000000', kind: 'whatsapp', provider,
    capabilities: getWhatsAppCapabilities(provider), status: 'connected',
    created_at: '2026-01-01T00:00:00Z', last_connected_at: null,
  };
}

const atEnv = { AFRICASTALKING_USERNAME: 'account', AFRICASTALKING_API_KEY: 'secret' } as NodeJS.ProcessEnv;
const TOKEN = '11111111-2222-4333-8444-555555555555';

describe('resolveCreateProvider', () => {
  it('keeps Baileys when the provider is omitted', () => {
    assert.equal(resolveCreateProvider(undefined), 'baileys');
    assert.equal(resolveCreateProvider(null), 'baileys');
  });

  it('accepts the known providers', () => {
    assert.equal(resolveCreateProvider('baileys'), 'baileys');
    assert.equal(resolveCreateProvider('africas_talking'), 'africas_talking');
  });

  it('rejects unknown providers with a 400', () => {
    for (const bad of ['twilio', '', 42, {}, '__proto__']) {
      assert.throws(() => resolveCreateProvider(bad), (e: { statusCode?: number }) => e.statusCode === 400);
    }
  });
});

describe('planChannelCreate', () => {
  it('starts Baileys channels as connecting with no inbound token', () => {
    assert.deepEqual(planChannelCreate('baileys', {} as NodeJS.ProcessEnv), {
      provider: 'baileys', status: 'connecting', inboundToken: null,
    });
  });

  it("connects Africa's Talking channels immediately with a fresh inbound token", () => {
    assert.deepEqual(planChannelCreate('africas_talking', atEnv, () => TOKEN), {
      provider: 'africas_talking', status: 'connected', inboundToken: TOKEN,
    });
  });

  it("generates a distinct UUID token per Africa's Talking channel by default", () => {
    const a = planChannelCreate('africas_talking', atEnv).inboundToken;
    const b = planChannelCreate('africas_talking', atEnv).inboundToken;
    assert.match(a!, /^[0-9a-f-]{36}$/);
    assert.notEqual(a, b);
  });

  it("requires both Africa's Talking deployment secrets", () => {
    const missing: NodeJS.ProcessEnv[] = [
      {},
      { AFRICASTALKING_USERNAME: 'account' },
      { AFRICASTALKING_API_KEY: 'secret' },
      { AFRICASTALKING_USERNAME: '  ', AFRICASTALKING_API_KEY: 'secret' },
    ];
    for (const env of missing) {
      assert.throws(
        () => planChannelCreate('africas_talking', env, () => TOKEN),
        (e: unknown) => e instanceof WhatsAppProviderError && e.type === 'config' && e.statusCode === 503,
      );
    }
  });

  it('never puts the secrets in the plan or in the error', () => {
    const plan = planChannelCreate('africas_talking', atEnv, () => TOKEN);
    assert.ok(!JSON.stringify(plan).includes('secret'));
    try {
      planChannelCreate('africas_talking', { AFRICASTALKING_API_KEY: 'secret' } as NodeJS.ProcessEnv);
    } catch (e) {
      assert.ok(!String((e as Error).message).includes('secret'));
    }
  });
});

describe('assertCapability', () => {
  it('allows Baileys groups and reconnect', () => {
    assert.doesNotThrow(() => assertCapability(channel('baileys'), 'groups'));
    assert.doesNotThrow(() => assertCapability(channel('baileys'), 'session_reconnect'));
  });

  it("rejects Africa's Talking groups and reconnect with a typed 400", () => {
    for (const capability of ['groups', 'session_reconnect'] as const) {
      assert.throws(
        () => assertCapability(channel('africas_talking'), capability),
        (e: { statusCode?: number; code?: string; capability?: string }) =>
          e.statusCode === 400 && e.code === 'unsupported_capability' && e.capability === capability,
      );
    }
  });

  it('derives capabilities from the provider rather than trusting a stale object', () => {
    const stale = { ...channel('africas_talking'), capabilities: getWhatsAppCapabilities('baileys') };
    assert.throws(() => assertCapability(stale, 'groups'));
  });

  it('rejects channels with no WhatsApp provider', () => {
    const email: Channel = { ...channel('baileys'), kind: 'email', provider: null, capabilities: undefined };
    assert.throws(() => assertCapability(email, 'groups'), (e: { statusCode?: number }) => e.statusCode === 400);
  });
});

describe('resolveWhatsAppTarget', () => {
  it("keeps direct sends on Africa's Talking", () => {
    assert.equal(resolveWhatsAppTarget(channel('africas_talking'), null, '+27821111111'), '+27821111111');
    assert.equal(resolveWhatsAppTarget(channel('africas_talking'), '  ', '+27821111111'), '+27821111111');
  });

  it('targets the group for Baileys group tickets', () => {
    assert.equal(resolveWhatsAppTarget(channel('baileys'), ' 1203@g.us ', '+27821111111'), '1203@g.us');
  });

  it("rejects a group target on Africa's Talking", () => {
    assert.throws(
      () => resolveWhatsAppTarget(channel('africas_talking'), '1203@g.us', '+27821111111'),
      (e: { statusCode?: number; code?: string }) => e.statusCode === 400 && e.code === 'unsupported_capability',
    );
  });
});

describe('redactInboundToken', () => {
  const withToken = (c: Channel): Channel => ({ ...c, inbound_token: TOKEN });
  const email: Channel = { ...channel('baileys'), kind: 'email', provider: null, capabilities: undefined };

  it('keeps the token for admins on WhatsApp and email channels', () => {
    assert.equal(redactInboundToken(withToken(channel('africas_talking')), 'admin').inbound_token, TOKEN);
    assert.equal(redactInboundToken(withToken(email), 'admin').inbound_token, TOKEN);
  });

  it('hides the token from non-admin staff and other roles', () => {
    for (const role of ['user', 'client', undefined]) {
      assert.equal(redactInboundToken(withToken(channel('africas_talking')), role).inbound_token, undefined);
      assert.equal(redactInboundToken(withToken(email), role).inbound_token, undefined);
    }
  });

  it('does not mutate the input and keeps other fields', () => {
    const input = withToken(channel('africas_talking'));
    const out = redactInboundToken(input, 'user');
    assert.equal(input.inbound_token, TOKEN);
    assert.equal(out.id, input.id);
    assert.equal(out.provider, 'africas_talking');
  });
});

describe('isUnsupportedCapabilityError', () => {
  it('matches only the typed capability error', () => {
    let caught: unknown;
    try { assertCapability(channel('africas_talking'), 'groups'); } catch (e) { caught = e; }
    assert.equal(isUnsupportedCapabilityError(caught), true);
    assert.equal(isUnsupportedCapabilityError(new Error('Channel not found')), false);
    assert.equal(isUnsupportedCapabilityError(Object.assign(new Error('x'), { statusCode: 400 })), false);
    assert.equal(isUnsupportedCapabilityError(null), false);
    assert.equal(isUnsupportedCapabilityError('unsupported_capability'), false);
  });
});

describe('serializeEmbeddedChannel', () => {
  const raw = (over: Record<string, unknown>) =>
    ({ ...channel('baileys'), capabilities: undefined, inbound_token: TOKEN, ...over }) as unknown as Channel;

  it('computes capabilities from provider for WhatsApp channels', () => {
    assert.deepEqual(
      serializeEmbeddedChannel(raw({ provider: 'africas_talking' }))?.capabilities,
      getWhatsAppCapabilities('africas_talking'),
    );
    assert.deepEqual(serializeEmbeddedChannel(raw({ provider: 'baileys' }))?.capabilities, getWhatsAppCapabilities('baileys'));
  });

  it('always strips the inbound token', () => {
    for (const c of [raw({ provider: 'africas_talking' }), raw({ kind: 'email', provider: null })]) {
      const out = serializeEmbeddedChannel(c) as Record<string, unknown>;
      assert.equal('inbound_token' in out, false);
    }
  });

  it('adds no capabilities to non-WhatsApp channels and passes null through', () => {
    assert.equal(serializeEmbeddedChannel(raw({ kind: 'email', provider: null }))?.capabilities, undefined);
    assert.equal(serializeEmbeddedChannel(null), null);
    assert.equal(serializeEmbeddedChannel(undefined), undefined);
  });
});

/**
 * Africa's Talking has no session and no reconnect, so `disconnected` is only ever set by an
 * outbound authentication failure. If sends were blocked there the channel could never come back —
 * the send IS the revalidation path. Baileys `disconnected` is a real dead session and stays closed.
 */
describe('canStaffSendOnChannel', () => {
  const at = (status: Channel['status']): Channel => ({ ...channel('africas_talking'), status });
  const baileys = (status: Channel['status']): Channel => ({ ...channel('baileys'), status });

  it('allows a disconnected Africa\'s Talking channel so the send can revalidate credentials', () => {
    assert.equal(canStaffSendOnChannel(at('disconnected')), true);
  });

  it('still refuses a disconnected Baileys channel', () => {
    assert.equal(canStaffSendOnChannel(baileys('disconnected')), false);
  });

  it('allows any connected channel, whatever the provider or kind', () => {
    assert.equal(canStaffSendOnChannel(at('connected')), true);
    assert.equal(canStaffSendOnChannel(baileys('connected')), true);
    assert.equal(canStaffSendOnChannel({ ...channel('baileys'), kind: 'email', provider: null }), true);
    assert.equal(canStaffSendOnChannel({ ...channel('baileys'), kind: 'portal', provider: null }), true);
  });

  it('refuses every other non-connected state, including on Africa\'s Talking', () => {
    // Only `disconnected` is the credential-failure state; `connecting` means never established.
    assert.equal(canStaffSendOnChannel(at('connecting')), false);
    assert.equal(canStaffSendOnChannel(baileys('connecting')), false);
  });

  it('refuses a missing channel', () => {
    assert.equal(canStaffSendOnChannel(null), false);
    assert.equal(canStaffSendOnChannel(undefined), false);
  });

  it('does not extend the exception to a non-WhatsApp channel that merely names the provider', () => {
    assert.equal(
      canStaffSendOnChannel({ ...channel('africas_talking'), kind: 'email', status: 'disconnected' }),
      false,
    );
  });
});
