'use strict';

require('../support/test_helper');

const assert = require('../support/assert');
const TestClient = require('../support/test-client');
const serverOptions = require('../../lib/cartodb/server_options');

const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;

const pointSleepSql = `
    SELECT
        pg_sleep(0.3),
        'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator,
        1 cartodb_id,
        2 val
`;

const validationPointSleepSql = `
    SELECT
        pg_sleep(0.3),
        ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
        1 cartodb_id,
        2 val
`;

const cartoCSSPoints = () => `
// cache buster: ${Date.now()}
#layer{
  marker-placement: point;
  marker-allow-overlap: true;
  marker-line-opacity: 0.2;
  marker-line-width: 0.5;
  marker-opacity: 1;
  marker-width: 5;
  marker-fill: red;
}`;

const createMapConfig = ({
    version = '1.6.0',
    type = 'cartodb',
    sql = pointSleepSql,
    cartocss = cartoCSSPoints(),
    cartocss_version = '2.3.0',
    interactivity = 'cartodb_id',
    countBy = 'cartodb_id',
    attributes
} = {}) => ({
    version,
    layers: [{
        type,
        options: {
            source: {
                id: 'a0'
            },
            cartocss,
            cartocss_version,
            attributes,
            interactivity
        }
    }],
    analyses: [
        {
            id: 'a0',
            type: 'source',
            params: {
                query: sql
            }
        }
    ],
    dataviews: {
        count: {
            source: {
                id: 'a0'
            },
            type: 'formula',
            options: {
                column: countBy,
                operation: 'count'
            }
        }
    }
});

const db_limit_error_message = 'You are over platform\'s limits: SQL query timeout error.' +
    ' Refactor your query before running again or contact CARTO support for more details.';

const DATASOURCE_TIMEOUT_ERROR = {
    errors: [db_limit_error_message],
    errors_with_context: [{
        type: 'limit',
        subtype: 'datasource',
        message: db_limit_error_message
    }]
};

describe('user database timeout limit', function () {
    describe('dataview', function () {
        beforeEach(function (done) {
            const mapconfig = createMapConfig();
            this.testClient = new TestClient(mapconfig, 1234);
            this.testClient.setUserDatabaseTimeoutLimit(200, done);
        });

        afterEach(function (done) {
            this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                if (err) {
                    return done(err);
                }
                this.testClient.drain(done);
            });
        });

        it('layergroup creation works but dataview request fails due to statement timeout', function (done) {
            const params = {
                response: {
                    status: 429,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                }
            };

            this.testClient.getDataview('count', params, (err, timeoutError) => {
                assert.ifError(err);

                assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);

                done();
            });
        });
    });

    describe('raster', function () {
        describe('while validating in layergroup creation', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({ sql: validationPointSleepSql });
                this.testClient = new TestClient(mapconfig, 1234);
                this.testClient.setUserDatabaseTimeoutLimit(200, done);
            });

            afterEach(function (done) {
                this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                    if (err) {
                        return done(err);
                    }
                    this.testClient.drain(done);
                });
            });

            it('fails due to statement timeout', function (done) {
                const expectedResponse = {
                    status: 429,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
                    assert.deepEqual(timeoutError, {
                        errors: [db_limit_error_message ],
                        errors_with_context: [{
                            type: 'limit',
                            subtype: 'datasource',
                            message: db_limit_error_message,
                            layer: { id: 'layer0', index: 0, type: 'mapnik' }
                        }]
                    });

                    done();
                });
            });
        });

        describe('fetching raster tiles', function () {
            describe('with user\'s timeout of 200 ms', function () {
                describe('with onTileErrorStrategy ENABLED', function () {
                    let onTileErrorStrategy;

                    beforeEach(function (done) {
                        onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
                        global.environment.enabledFeatures.onTileErrorStrategy = true;

                        const mapconfig = createMapConfig();
                        this.testClient = new TestClient(mapconfig, 1234);
                        const expectedResponse = {
                            status: 200,
                            headers: {
                                'Content-Type': 'application/json; charset=utf-8'
                            }
                        };

                        this.testClient.setUserDatabaseTimeoutLimit(200, (err) => {
                            if (err) {
                                return done(err);
                            }

                            this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
                                if (err) {
                                    return done(err);
                                }

                                this.layergroupid = res.layergroupid;

                                done();
                            });
                        });
                    });

                    afterEach(function (done) {
                        global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
                        this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                            if (err) {
                                return done(err);
                            }
                            this.testClient.drain(done);
                        });
                    });

                    it('"png" fails due to statement timeout', function (done) {
                        const params = {
                            layergroupid: this.layergroupid,
                            format: 'png',
                            layers: [ 0 ]
                        };

                        this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
                            assert.ifError(err);

                            assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
                                assert.ifError(err);
                                done();
                            });
                        });
                    });

                    it('"static png" fails due to statement timeout', function (done) {
                        const params = {
                            layergroupid: this.layergroupid,
                            zoom: 0,
                            lat: 0,
                            lng: 0,
                            width: 256,
                            height: 256,
                            format: 'png'
                        };

                        this.testClient.getStaticCenter(params, function (err, res, tile) {
                            assert.ifError(err);

                            assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
                                assert.ifError(err);
                                done();
                            });
                        });
                    });
                });

                describe('with onTileErrorStrategy DISABLED', function () {
                    let onTileErrorStrategy;

                    beforeEach(function (done) {
                        onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
                        global.environment.enabledFeatures.onTileErrorStrategy = false;

                        const mapconfig = createMapConfig();
                        this.testClient = new TestClient(mapconfig, 1234);
                        const expectedResponse = {
                            status: 200,
                            headers: {
                                'Content-Type': 'application/json; charset=utf-8'
                            }
                        };

                        this.testClient.setUserDatabaseTimeoutLimit(200, (err) => {
                            if (err) {
                                return done(err);
                            }

                            this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
                                if (err) {
                                    return done(err);
                                }

                                this.layergroupid = res.layergroupid;

                                done();
                            });
                        });
                    });

                    afterEach(function (done) {
                        global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;

                        this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                            if (err) {
                                return done(err);
                            }

                            this.testClient.drain(done);
                        });
                    });

                    it('"png" fails due to statement timeout', function (done) {
                        const params = {
                            layergroupid: this.layergroupid,
                            format: 'png',
                            layers: [ 0 ],
                            response: {
                                status: 429,
                                headers: {
                                    'Content-Type': 'application/json; charset=utf-8'
                                }
                            }
                        };

                        this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
                            assert.ifError(err);

                            assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);

                            done();
                        });
                    });

                    it('"static png" fails due to statement timeout', function (done) {
                        const params = {
                            layergroupid: this.layergroupid,
                            zoom: 0,
                            lat: 0,
                            lng: 0,
                            width: 256,
                            height: 256,
                            format: 'png',
                            response: {
                                status: 429,
                                headers: {
                                    'Content-Type': 'application/json; charset=utf-8'
                                }
                            }
                        };

                        this.testClient.getStaticCenter(params, (err, res, timeoutError) => {
                            assert.ifError(err);

                            assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);

                            done();
                        });
                    });
                });
            });
        });
    });

    describe('vector', function () {
        describe('while validating in layergroup creation', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({ sql: validationPointSleepSql });
                this.testClient = new TestClient(mapconfig, 1234);
                this.testClient.setUserDatabaseTimeoutLimit(200, done);
            });

            afterEach(function (done) {
                this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                    if (err) {
                        return done(err);
                    }
                    this.testClient.drain(done);
                });
            });

            it('fails due to statement timeout', function (done) {
                const expectedResponse = {
                    status: 429,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
                    assert.deepEqual(timeoutError, {
                        errors: [ db_limit_error_message ],
                        errors_with_context: [{
                            type: 'limit',
                            subtype: 'datasource',
                            message: db_limit_error_message,
                            layer: { id: 'layer0', index: 0, type: 'mapnik' }
                        }]
                    });

                    done();
                });
            });
        });

        const describe_pg = process.env.POSTGIS_VERSION >= '20400' ? describe : describe.skip;
        describe('fetching vector tiles via mapnik renderer', () => { testFetchingVectorTiles(false); });
        describe_pg('fetching vector tiles via postgis renderer', () => { testFetchingVectorTiles(true); });

        function testFetchingVectorTiles(usePostGIS) {
            const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;

            before(function () {
                serverOptions.renderer.mvt.usePostGIS = usePostGIS;
            });

            after(function () {
                serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
            });


            beforeEach(function (done) {
                const mapconfig = createMapConfig();
                this.testClient = new TestClient(mapconfig, 1234);
                const expectedResponse = {
                    status: 200,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
                    if (err) {
                        return done(err);
                    }

                    this.layergroupid = res.layergroupid;

                    done();
                });
            });

            afterEach(function (done) {
                this.testClient.drain(done);
            });

            describe('with user\'s timeout of 200 ms', function () {
                beforeEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(200, done);
                });

                afterEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(0, done);
                });

                it('"mvt" fails due to statement timeout', function (done) {
                    const params = {
                        layergroupid: this.layergroupid,
                        format: 'mvt',
                        layers: [ 0 ],
                        response: {
                            status: 429,
                            headers: {
                                'Content-Type': 'application/x-protobuf'
                            }
                        }
                    };

                    this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
                        assert.ifError(err);

                        var tileJSON = tile.toJSON();
                        assert.equal(Array.isArray(tileJSON), true);
                        assert.equal(tileJSON.length, 2);
                        assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
                        assert.equal(tileJSON[1].name, 'errorTileStripesLayer');

                        done();
                    });

                });
            });
        }
    });


    describe('interactivity', function () {
        describe('while validating in layergroup creation', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({ sql: validationPointSleepSql, interactivity: 'val' });
                this.testClient = new TestClient(mapconfig, 1234);
                this.testClient.setUserDatabaseTimeoutLimit(200, done);
            });

            afterEach(function (done) {
                this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                    if (err) {
                        return done(err);
                    }
                    this.testClient.drain(done);
                });
            });

            it('fails due to statement timeout', function (done) {
                const expectedResponse = {
                    status: 429,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
                    assert.deepEqual(timeoutError, {
                        errors: [ db_limit_error_message ],
                        errors_with_context: [{
                            type: 'limit',
                            subtype: 'datasource',
                            message: db_limit_error_message,
                            layer: { id: 'layer0', index: 0, type: 'mapnik' }
                        }]
                    });

                    done();
                });
            });
        });

        describe('fetching interactivity tiles', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({ interactivity: 'val' });
                this.testClient = new TestClient(mapconfig, 1234);
                const expectedResponse = {
                    status: 200,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
                    if (err) {
                        return done(err);
                    }

                    this.layergroupid = res.layergroupid;

                    done();
                });
            });

            afterEach(function (done) {
                this.testClient.drain(done);
            });

            describe('with user\'s timeout of 200 ms', function () {
                beforeEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(200, done);
                });

                afterEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(0, done);
                });

                it('"grid.json" fails due to statement timeout', function (done) {
                    const params = {
                        layergroupid: this.layergroupid,
                        format: 'grid.json',
                        layers: 'mapnik',
                        response: {
                            status: 429,
                            headers: {
                                'Content-Type': 'application/json; charset=utf-8'
                            }
                        }
                    };

                    this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
                        assert.ifError(err);

                        assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);

                        done();
                    });
                });
            });
        });
    });

    describe('torque', function () {
        describe('while validating in layergroup creation', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({
                    type: 'torque',
                    cartocss: TestClient.CARTOCSS.TORQUE
                });
                this.testClient = new TestClient(mapconfig, 1234);
                this.testClient.setUserDatabaseTimeoutLimit(200, done);
            });

            afterEach(function (done) {
                this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                    if (err) {
                        return done(err);
                    }
                    this.testClient.drain(done);
                });
            });

            it('fails due to statement timeout', function (done) {
                const expectedResponse = {
                    status: 429,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
                    assert.deepEqual(timeoutError, {
                        errors: [db_limit_error_message],
                        errors_with_context: [{
                            type: 'limit',
                            subtype: 'datasource',
                            message: db_limit_error_message,
                            layer: { id: 'torque-layer0', index: 0, type: 'torque' }
                        }]
                    });

                    done();
                });
            });
        });

        describe('fetching torque tiles', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({
                    type: 'torque',
                    cartocss: TestClient.CARTOCSS.TORQUE
                });
                this.testClient = new TestClient(mapconfig, 1234);
                const expectedResponse = {
                    status: 200,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
                    if (err) {
                        return done(err);
                    }

                    this.layergroupid = res.layergroupid;

                    done();
                });
            });

            afterEach(function (done) {
                this.testClient.drain(done);
            });

            describe('with user\'s timeout of 200 ms', function () {
                beforeEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(200, done);
                });

                afterEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(0, done);
                });

                it('"torque.json" fails due to statement timeout', function (done) {
                    const params = {
                        layergroupid: this.layergroupid,
                        format: 'torque.json',
                        layers: [ 0 ],
                        response: {
                            status: 429,
                            headers: {
                                'Content-Type': 'application/json; charset=utf-8'
                            }
                        }
                    };

                    this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
                        assert.ifError(err);

                        assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);

                        done();
                    });
                });

                it('".png" fails due to statement timeout', function (done) {
                    const params = {
                        layergroupid: this.layergroupid,
                        format: 'torque.png',
                        layers: [ 0 ],
                        response: {
                            status: 429,
                            headers: {
                                'Content-Type': 'application/json; charset=utf-8'
                            }
                        }
                    };

                    this.testClient.getTile(0, 0, 0, params, (err, res, attributes) => {
                        assert.ifError(err);

                        assert.deepEqual(attributes, DATASOURCE_TIMEOUT_ERROR);

                        done();
                    });
                });
            });
        });
    });

    describe('attributes:', function () {
        describe('while validating in map instatiation', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({
                    attributes: {
                        id: 'cartodb_id',
                        columns: [ 'val' ]
                    }
                });
                this.testClient = new TestClient(mapconfig, 1234);
                this.testClient.setUserDatabaseTimeoutLimit(200, done);
            });

            afterEach(function (done) {
                this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
                    if (err) {
                        return done(err);
                    }
                    this.testClient.drain(done);
                });
            });

            it('layergroup creation fails due to statement timeout', function (done) {
                const expectedResponse = {
                    status: 429,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
                    assert.deepEqual(timeoutError, {
                        errors: [db_limit_error_message],
                        errors_with_context: [{
                            type: 'limit',
                            subtype: 'datasource',
                            message: db_limit_error_message,
                            layer: {
                                id: 'layer0',
                                index: 0,
                                type: 'mapnik'
                            }
                        }]
                    });

                    done();
                });
            });
        });

        describe('fetching by feature id', function () {
            beforeEach(function (done) {
                const mapconfig = createMapConfig({
                    attributes: {
                        id: 'cartodb_id',
                        columns: [ 'val' ]
                    }
                });

                this.testClient = new TestClient(mapconfig, 1234);

                const expectedResponse = {
                    status: 200,
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    }
                };

                this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
                    if (err) {
                        return done(err);
                    }

                    this.layergroupid = res.layergroupid;

                    done();
                });
            });

            afterEach(function (done) {
                this.testClient.drain(done);
            });

            describe('with user\'s timeout of 200 ms', function () {
                beforeEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(200, done);
                });

                afterEach(function (done) {
                    this.testClient.setUserDatabaseTimeoutLimit(0, done);
                });

                it('fails due to statement timeout', function (done) {
                    const params = {
                        layergroupid: this.layergroupid,
                        featureId: 1,
                        layer: 0,
                        response: {
                            status: 429,
                            headers: {
                                'Content-Type': 'application/json; charset=utf-8'
                            }
                        }
                    };

                    this.testClient.getAttributes(params, (err, res, timeoutError) => {
                        assert.ifError(err);

                        assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);

                        done();
                    });
                });
            });
        });
    });
});
