/**
 * RED-GREEN tests for the Slack incoming-webhook adapter (Phase 2a HITL).
 *
 * Covers createSlackWebhookSender + ApprovalChannelDispatcher slack routing.
 * Uses the same injectable HttpsTransport pattern as the Resend/Twilio tests.
 *
 * @connexum/ai-governance  F-NEW-PRODUCT-HITL-SLACK-PHASE-2A
 */
import { describe, it } from 'node:test';
import * as assert from 'node:assert/strict';
import { createSlackWebhookSender, } from './approval-channel-adapters.js';
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
function recordingTransport(response = { status: 200, bodyText: 'ok' }) {
    const calls = [];
    return {
        calls,
        transport: async (req) => {
            calls.push(req);
            return response;
        },
    };
}
const TEST_WEBHOOK = 'https://hooks.slack.com/services/T000/B000/xxxx';
const TEST_CONTEXT = {
    ticketId: 'tic-slack-1',
    orgId: 'org-acme',
    agentId: 'agent-007',
    toolName: 'Bash',
    riskLevel: 'high',
    summary: 'rm -rf /tmp/important',
    approveUrl: 'https://my-cc.io/approve/tic-slack-1?sig=abc',
    denyUrl: 'https://my-cc.io/deny/tic-slack-1?sig=abc',
    slaDeadline: '2026-05-22T12:00:00.000Z',
};
// ---------------------------------------------------------------------------
// 1. Success (HTTP 200) -> ok: true
// ---------------------------------------------------------------------------
describe('createSlackWebhookSender — success path', () => {
    it('returns ok:true on HTTP 200 from the webhook endpoint', async () => {
        const t = recordingTransport({ status: 200, bodyText: 'ok' });
        const sender = createSlackWebhookSender({ transport: t.transport });
        const result = await sender.sendApproval({
            webhookUrl: TEST_WEBHOOK,
            messageText: 'Approval needed',
            context: TEST_CONTEXT,
        });
        assert.equal(result.ok, true);
        assert.equal(result.error, undefined);
    });
    it('makes exactly one POST to the webhook hostname + path', async () => {
        const t = recordingTransport();
        const sender = createSlackWebhookSender({ transport: t.transport });
        await sender.sendApproval({
            webhookUrl: TEST_WEBHOOK,
            messageText: 'test',
            context: TEST_CONTEXT,
        });
        assert.equal(t.calls.length, 1);
        const call = t.calls[0];
        assert.equal(call.method, 'POST');
        assert.equal(call.hostname, 'hooks.slack.com');
        assert.equal(call.path, '/services/T000/B000/xxxx');
        assert.equal(call.headers['content-type'], 'application/json');
    });
});
// ---------------------------------------------------------------------------
// 2. Failure (HTTP 500) -> ok: false, error contains status
// ---------------------------------------------------------------------------
describe('createSlackWebhookSender — HTTP error path', () => {
    it('returns ok:false and error containing status when webhook returns 5xx', async () => {
        const t = recordingTransport({ status: 500, bodyText: 'Internal Server Error' });
        const sender = createSlackWebhookSender({ transport: t.transport });
        const result = await sender.sendApproval({
            webhookUrl: TEST_WEBHOOK,
            messageText: 'test',
            context: TEST_CONTEXT,
        });
        assert.equal(result.ok, false);
        assert.match(result.error ?? '', /500/);
    });
});
// ---------------------------------------------------------------------------
// 3. No webhookUrl + no defaultWebhookUrl -> ok: false, error: 'no webhook URL'
// ---------------------------------------------------------------------------
describe('createSlackWebhookSender — missing URL', () => {
    it('returns ok:false with "no webhook URL" when both webhookUrl and default are absent', async () => {
        const t = recordingTransport();
        const sender = createSlackWebhookSender({ transport: t.transport });
        // No defaultWebhookUrl in options; empty string in call arg falls back to default.
        const result = await sender.sendApproval({
            webhookUrl: '',
            messageText: 'test',
            context: TEST_CONTEXT,
        });
        assert.equal(result.ok, false);
        assert.equal(result.error, 'no webhook URL');
        assert.equal(t.calls.length, 0, 'no HTTP call should be made');
    });
    it('uses defaultWebhookUrl when per-recipient webhookUrl is empty', async () => {
        const t = recordingTransport();
        const sender = createSlackWebhookSender({
            defaultWebhookUrl: TEST_WEBHOOK,
            transport: t.transport,
        });
        const result = await sender.sendApproval({
            webhookUrl: '',
            messageText: 'test',
            context: TEST_CONTEXT,
        });
        assert.equal(result.ok, true);
        assert.equal(t.calls[0].hostname, 'hooks.slack.com');
    });
});
// ---------------------------------------------------------------------------
// 4. Payload shape: approve/deny buttons with correct URLs
// ---------------------------------------------------------------------------
describe('createSlackWebhookSender — payload shape', () => {
    it('Block Kit payload contains approve+deny action buttons with correct URLs', async () => {
        const t = recordingTransport();
        const sender = createSlackWebhookSender({ transport: t.transport });
        await sender.sendApproval({
            webhookUrl: TEST_WEBHOOK,
            messageText: 'Needs your attention',
            context: TEST_CONTEXT,
        });
        const body = JSON.parse(t.calls[0].body ?? '{}');
        // text fallback
        assert.equal(body.text, 'Needs your attention');
        // section block with mrkdwn
        const section = body.blocks.find((b) => b.type === 'section');
        assert.ok(section, 'section block must exist');
        assert.equal(section?.text?.type, 'mrkdwn');
        // actions block
        const actions = body.blocks.find((b) => b.type === 'actions');
        assert.ok(actions, 'actions block must exist');
        assert.ok(Array.isArray(actions?.elements), 'actions.elements must be an array');
        const approve = actions?.elements?.find((e) => e.text.text === 'Approve');
        const deny = actions?.elements?.find((e) => e.text.text === 'Deny');
        assert.ok(approve, 'Approve button must exist');
        assert.equal(approve?.url, TEST_CONTEXT.approveUrl);
        assert.equal(approve?.style, 'primary');
        assert.ok(deny, 'Deny button must exist');
        assert.equal(deny?.url, TEST_CONTEXT.denyUrl);
        assert.equal(deny?.style, 'danger');
    });
});
// ---------------------------------------------------------------------------
// 5. ApprovalChannelDispatcher routes 'slack' channel to slackSender
// ---------------------------------------------------------------------------
describe('ApprovalChannelDispatcher — slack channel routing', () => {
    it('routes channel=slack to slackSender.sendApproval and returns delivered:true on success', async () => {
        const { ApprovalChannelDispatcher } = await import('./approval-notifications.js');
        const t = recordingTransport({ status: 200, bodyText: 'ok' });
        const sender = createSlackWebhookSender({ transport: t.transport });
        const dispatcher = new ApprovalChannelDispatcher({ slackSender: sender });
        const outcomes = await dispatcher.dispatch(TEST_CONTEXT, [
            {
                reviewerId: 'rev-slack-1',
                name: 'Slack Reviewer',
                webhookUrl: TEST_WEBHOOK,
                channels: ['slack'],
            },
        ]);
        assert.equal(outcomes.length, 1);
        assert.equal(outcomes[0].channel, 'slack');
        assert.equal(outcomes[0].delivered, true);
        assert.equal(t.calls.length, 1);
    });
    it('auto-adds slack channel when recipient has webhookUrl but no explicit channels[]', async () => {
        const { ApprovalChannelDispatcher } = await import('./approval-notifications.js');
        const t = recordingTransport({ status: 200, bodyText: 'ok' });
        const sender = createSlackWebhookSender({ transport: t.transport });
        const dispatcher = new ApprovalChannelDispatcher({ slackSender: sender });
        // channels is omitted — only 'dashboard' would normally apply, but
        // webhookUrl presence should auto-include 'slack'.
        const outcomes = await dispatcher.dispatch(TEST_CONTEXT, [
            {
                reviewerId: 'rev-auto',
                name: 'Auto Reviewer',
                webhookUrl: TEST_WEBHOOK,
                // no channels[] specified
            },
        ]);
        const slackOutcome = outcomes.find((o) => o.channel === 'slack');
        assert.ok(slackOutcome, 'slack outcome must be present even without explicit channels[]');
        assert.equal(slackOutcome.delivered, true);
    });
    it('returns delivered:false with error when no webhookUrl on slack channel', async () => {
        const { ApprovalChannelDispatcher } = await import('./approval-notifications.js');
        const t = recordingTransport();
        const sender = createSlackWebhookSender({ transport: t.transport });
        const dispatcher = new ApprovalChannelDispatcher({ slackSender: sender });
        const outcomes = await dispatcher.dispatch(TEST_CONTEXT, [
            {
                reviewerId: 'rev-no-url',
                name: 'No URL Reviewer',
                // webhookUrl absent
                channels: ['slack'],
            },
        ]);
        assert.equal(outcomes[0].delivered, false);
        assert.match(outcomes[0].error ?? '', /webhook URL/i);
        assert.equal(t.calls.length, 0);
    });
});
//# sourceMappingURL=approval-channel-slack-adapter.test.js.map