import assert from 'assert/strict';
import sinon from 'sinon';

import {InMemoryMailEventRepository as MailEventRepository} from '../src/InMemoryMailEventRepository';
import {MailEvent} from '../src/MailEvent';
import {MailEventService} from '../src/MailEventService';

const makePayloadEvent = (
    type: string,
    timestamp = Date.now()
) => ({
    id: 'event-id',
    timestamp: timestamp / 1000,
    event: type,
    message: {
        headers: {
            'message-id': 'message-id'
        }
    },
    recipient: 'message-recipient'
});

const PAYLOAD_SIGNING_KEY = 'abc123';

describe('MailEventService', function () {
    let repository: sinon.SinonStubbedInstance<MailEventRepository>;
    let config: sinon.SinonStubbedInstance<any>;
    let labs: sinon.SinonStubbedInstance<any>;
    let service: MailEventService;

    beforeEach(function () {
        repository = sinon.createStubInstance(MailEventRepository);
        labs = {
            isSet: sinon.stub()
                .withArgs(MailEventService.LABS_KEY)
                .returns(true)
        };
        config = {
            get: sinon.stub()
                .withArgs(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY)
                .returns(PAYLOAD_SIGNING_KEY)
        };
        service = new MailEventService(repository, config, labs);
    });

    describe('processPayload', function () {
        it('should reject if labs flag is false', async function () {
            labs.isSet.withArgs(MailEventService.LABS_KEY).returns(false);

            await assert.rejects(
                service.processPayload({} as any),
                {
                    name: 'NotFoundError',
                    message: 'Resource could not be found.'
                }
            );
        });

        it('should reject if payload signing key is invalid', async function () {
            config.get.withArgs(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY).returns(undefined);

            await assert.rejects(
                service.processPayload({} as any),
                {
                    name: 'InternalServerError',
                    message: 'payload signing key is not configured'
                }
            );
        });

        it('should reject if payload verification fails', async function () {
            await assert.rejects(
                service.processPayload({
                    signature: 'foobarbaz',
                    mail_events: [
                        makePayloadEvent('opened')
                    ]
                } as any),
                {
                    name: 'UnauthorizedError',
                    message: '"signature" is invalid'
                }
            );
        });

        it('should store a single event', async function () {
            const payloadEvent = makePayloadEvent('opened');

            // Ensure a fixed timestamp is used so that we know the signature up front
            payloadEvent.timestamp = 1686665992511 / 1000;

            const payload = {
                signature: '9f2567330688b82759600fad93c93b3e8f571d397c33688a8620400af20b79b3',
                mail_events: [
                    payloadEvent
                ]
            };

            await service.processPayload(payload);

            const storedEvent = repository.save.getCall(0).args[0];

            assert.ok(storedEvent instanceof MailEvent);
            assert.equal(storedEvent.id, payloadEvent.id);
        });

        it('should store multiple events', async function () {
            const events = [
                makePayloadEvent('opened'),
                makePayloadEvent('opened')
            ];

            // Ensure fixed timestamps are used so that we know the signature up front
            events[0].timestamp = 1686665992511 / 1000;
            events[1].timestamp = 1686665992512 / 1000;

            const payload = {
                signature: '02959cc9731ee575b66969a508f545d19c5968b42a03fa398ce9c93d8e7df0a5',
                mail_events: events
            };

            await service.processPayload(payload);

            assert.ok(repository.save.calledTwice);
        });

        it('should ignore unknown events', async function () {
            const events = [
                makePayloadEvent('unknown-event'),
                makePayloadEvent('opened')
            ];

            // Ensure fixed timestamps are used so that we know the signature up front
            events[0].timestamp = 1686665992511 / 1000;
            events[1].timestamp = 1686665992512 / 1000;

            const payload = {
                signature: 'd6de350faa9ec56d739ec7ffd5cb4230f90f583df05fe59a6c1a41afac7048df',
                mail_events: events
            };

            await service.processPayload(payload);

            assert.ok(repository.save.calledOnce);
            assert.equal(repository.save.getCall(0).args[0].type, 'opened');
        });

        it('should ensure event timestamps are converted to ms', async function () {
            const payloadEvent = makePayloadEvent('opened');

            // Ensure a fixed timestamp is used so that we know the signature up front
            payloadEvent.timestamp = 1686665992511 / 1000;

            const payload = {
                signature: '9f2567330688b82759600fad93c93b3e8f571d397c33688a8620400af20b79b3',
                mail_events: [
                    payloadEvent
                ]
            };

            await service.processPayload(payload);

            assert.ok(repository.save.calledOnce);

            const storedEvent = repository.save.getCall(0).args[0];

            assert.equal(storedEvent.timestampMs, payloadEvent.timestamp * 1000);
        });

        it('should reject if an event can not be stored', async function () {
            const payloadEvent = makePayloadEvent('opened');

            // Ensure a fixed timestamp is used so that we know the signature up front
            payloadEvent.timestamp = 1686665992511 / 1000;

            const payload = {
                signature: '9f2567330688b82759600fad93c93b3e8f571d397c33688a8620400af20b79b3',
                mail_events: [
                    payloadEvent
                ]
            };

            repository.save.rejects(new Error('foobarbaz'));

            await assert.rejects(
                service.processPayload(payload),
                {
                    name: 'InternalServerError',
                    message: 'Event could not be stored'
                }
            );
        });
    });

    describe('validatePayload', function () {
        it('should validate that the payload contains a signature', function () {
            assert.throws(
                () => service.validatePayload({} as any),
                {
                    name: 'ValidationError',
                    message: 'Payload is missing "signature"'
                }
            );
        });

        it('should validate that the payload contains a valid signature', function () {
            assert.throws(() => {
                service.validatePayload({
                    signature: {}
                } as any);
            }, {
                name: 'ValidationError',
                message: '"signature" is invalid'
            });
        });

        it('should validate that the payload contains events', function () {
            assert.throws(() => {
                service.validatePayload({
                    signature: 'foobarbaz'
                } as any);
            }, {
                name: 'ValidationError',
                message: 'Payload is missing "mail_events"'
            });
        });

        it('should validate that the payload contains valid events', function () {
            assert.throws(() => {
                service.validatePayload({
                    signature: 'foobarbaz',
                    mail_events: {}
                } as any);
            }, {
                name: 'ValidationError',
                message: '"mail_events" is not an array'
            });
        });

        it('should validate that events in the payload have an id', function () {
            const malformedPayloadEvent = makePayloadEvent('opened') as any;
            delete malformedPayloadEvent.id;

            const payload = {
                signature: 'foobarbaz',
                mail_events: [
                    makePayloadEvent('opened'),
                    malformedPayloadEvent
                ]
            };

            assert.throws(
                () => service.validatePayload(payload),
                {
                    name: 'ValidationError',
                    message: 'Event [1] is missing "id"'
                }
            );
        });

        it('should validate that events in the payload have an timestamp', function () {
            const malformedPayloadEvent = makePayloadEvent('opened') as any;
            delete malformedPayloadEvent.timestamp;

            const payload = {
                signature: 'foobarbaz',
                mail_events: [
                    makePayloadEvent('opened'),
                    malformedPayloadEvent
                ]
            };

            assert.throws(
                () => service.validatePayload(payload),
                {
                    name: 'ValidationError',
                    message: 'Event [1] is missing "timestamp"'
                }
            );
        });

        it('should validate that events in the payload have an event', function () {
            const malformedPayloadEvent = makePayloadEvent('opened') as any;
            delete malformedPayloadEvent.event;

            const payload = {
                signature: 'foobarbaz',
                mail_events: [
                    makePayloadEvent('opened'),
                    malformedPayloadEvent
                ]
            };

            assert.throws(
                () => service.validatePayload(payload),
                {
                    name: 'ValidationError',
                    message: 'Event [1] is missing "event"'
                }
            );
        });

        it('should validate that events in the payload have a message', function () {
            const malformedPayloadEvent = makePayloadEvent('opened') as any;
            delete malformedPayloadEvent.message;

            const payload = {
                signature: 'foobarbaz',
                mail_events: [
                    makePayloadEvent('opened'),
                    malformedPayloadEvent
                ]
            };

            assert.throws(
                () => service.validatePayload(payload),
                {
                    name: 'ValidationError',
                    message: 'Event [1] is missing "message"'
                }
            );
        });

        it('should validate that events in the payload have a recipient', function () {
            const malformedPayloadEvent = makePayloadEvent('opened') as any;
            delete malformedPayloadEvent.recipient;

            const payload = {
                signature: 'foobarbaz',
                mail_events: [
                    makePayloadEvent('opened'),
                    malformedPayloadEvent
                ]
            };

            assert.throws(
                () => service.validatePayload(payload),
                {
                    name: 'ValidationError',
                    message: 'Event [1] is missing "recipient"'
                }
            );
        });

        it('should validate that "message.headers.message-id" is present on an event', function () {
            const malformedPayloadEvent = makePayloadEvent('opened') as any;
            delete malformedPayloadEvent.message.headers;

            const payload = {
                signature: 'foobarbaz',
                mail_events: [
                    makePayloadEvent('opened'),
                    malformedPayloadEvent
                ]
            };

            assert.throws(
                () => service.validatePayload(payload),
                {
                    name: 'ValidationError',
                    message: 'Event [1] is missing "message.headers.message-id"'
                }
            );
        });
    });
});
