'use strict';

var test = require('tape');
var mbgl = require('../../index');
var fs = require('fs');
var path = require('path');
var style = require('../fixtures/style.json');

test('Map', function(t) {
    // This test is skipped because of the req.respond shim in index.js
    // which will always call new mbgl.Map()
    t.skip('must be constructed with new', function(t) {
        t.throws(function() {
            mbgl.Map();
        }, /Use the new operator to create new Map objects/);

        t.end();
    });

    t.test('must be constructed with options object', function(t) {
        t.throws(function() {
            new mbgl.Map();
        }, /Requires an options object as first argument/);

        t.throws(function() {
            new mbgl.Map('options');
        }, /Requires an options object as first argument/);

        t.end();
    });

    t.test('requires request property', function(t) {
        var options = {};

        t.throws(function() {
            new mbgl.Map(options);
        }, /Options object must have a 'request' method/);

        options.request = 'test';
        t.throws(function() {
            new mbgl.Map(options);
        }, /Options object must have a 'request' method/);

        options.request = function() {};
        t.doesNotThrow(function() {
            var map = new mbgl.Map(options);
            map.release();
        });

        t.end();
    });

    t.test('optional cancel property must be a function', function(t) {
        var options = {
            request: function() {}
        };

        options.cancel = 'test';
        t.throws(function() {
            new mbgl.Map(options);
        }, /Options object 'cancel' property must be a function/);

        options.cancel = function() {};
        t.doesNotThrow(function() {
            var map = new mbgl.Map(options);
            map.release();
        });

        t.end();
    });


    t.test('optional ratio property must be a number', function(t) {
        var options = {
            request: function() {}
        };

        options.ratio = 'test';
        t.throws(function() {
            new mbgl.Map(options);
        }, /Options object 'ratio' property must be a number/);

        options.ratio = 1.0;
        t.doesNotThrow(function() {
            var map = new mbgl.Map(options);
            map.release();
        });

        t.end();
    });

    t.test('instanceof mbgl.Map', function(t) {
        var options = {
            request: function() {},
            ratio: 1
        };

        var map = new mbgl.Map(options);

        t.ok(map instanceof mbgl.Map);
        t.equal(map.__proto__, mbgl.Map.prototype);

        var keys = Object.keys(mbgl.Map.prototype);

        t.deepEqual(keys, [
            'load',
            'loaded',
            'render',
            'release',
            'addClass',
            'addSource',
            'addLayer',
            'removeLayer',
            'addImage',
            'removeImage',
            'setLayoutProperty',
            'setPaintProperty',
            'setFilter',
            'setCenter',
            'setZoom',
            'setBearing',
            'setPitch',
            'dumpDebugLogs',
            'queryRenderedFeatures'
        ]);

        for (var key in keys) {
            t.equal(map[key], mbgl.Map.prototype[key]);
        }

        t.end();
    });

    t.test('.addImage', function(t) {
        var options = {
            request: function() {},
            ratio: 1
        };

        t.test('requires 3 arguments', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage();
            }, /Three arguments required/);

            map.release();
            t.end();
        });

        t.test('requires image argument to be an object', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage('foo', '', {});
            }, /Second argument must be an object/);

            map.release();
            t.end();
        });

        t.test('requires options argument to have a height param', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage('foo', {}, {
                    width: 40,
                    pixelRatio: 2
                });
            }, /height parameter required/);

            map.release();
            t.end();
        });

        t.test('requires options argument to have a pixelRatio param', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage('foo', {}, {
                    width: 40,
                    height: 40
                });
            }, /pixelRatio parameter required/);

            map.release();
            t.end();
        });

        t.test('requires specified height to be actual height of image', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage('foo', new Buffer(''), {
                    width: 401,
                    height: 400,
                    pixelRatio: 1
                }, 'Image size does not match buffer size');
            });

            map.release();
            t.end();
        });

        t.test('requires height and width to be less than 1024', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage('foo', new Buffer(''), {
                    width: 1025,
                    height: 1025,
                    pixelRatio: 1
                }, 'Max height and width is 1024');
            });

            map.release();
            t.end();
        });


        t.test('requires specified height to be actual height of image', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.addImage('foo', new Buffer('   '), {
                    width: 401,
                    height: 400,
                    pixelRatio: 1
                }, 'Image size does not match buffer size');
            });

            map.release();
            t.end();
        });

        t.test('No error', function(t) {
            var map = new mbgl.Map(options);

            t.doesNotThrow(function() {
                map.addImage('foo', new Buffer('    '), {
                    width: 1,
                    height: 1,
                    pixelRatio: 1
                });
            });

            map.release();
            t.end();
        });
    });

    t.test('.removeImage', function(t) {
        var options = {
            request: function() {},
            ratio: 1
        };

        t.test('requires one argument', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.removeImage();
            }, /One argument required/);

            map.release();
            t.end();
        });

        t.test('requires string as first argument', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.removeImage({});
            }, /First argument must be a string/);

            map.release();
            t.end();
        });

        t.test('removes image', function(t) {
            var map = new mbgl.Map(options);

            t.doesNotThrow(function() {
                map.removeImage('fooBar');
            });

            map.release();
            t.end();
        });
    });

    t.test('.load', function(t) {
        var options = {
            request: function() {},
            ratio: 1
        };

        t.test('requires a map style as first argument', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.load();
            }, /Requires a map style as first argument/);

            map.release();
            t.end();
        });

        t.test('requires a map style to be a valid style JSON', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.load('foo bar');
            }, /Requires a map style to be a valid style JSON/);

            t.throws(function() {
                map.load('""');
            }, /Requires a map style to be a valid style JSON/);

            map.release();
            t.end();
        });

        t.test('expect either an object or array at root', { timeout: 1000 }, function(t) {
            var map = new mbgl.Map(options);

            mbgl.once('message', function(msg) {
                t.equal(msg.severity, 'ERROR');
                t.equal(msg.class, 'ParseStyle');
                t.ok(msg.text.match(/Invalid value/));

                map.release();
                t.end();
            });

            t.throws(function() {
                map.load('invalid');
            }, /Requires a map style to be a valid style JSON/);
        });

        t.test('accepts an empty stylesheet string', function(t) {
            var map = new mbgl.Map(options);

            t.doesNotThrow(function() {
                map.load('{}');
            });

            map.release();
            t.end();
        });

        t.test('accepts a JSON stylesheet', { timeout: 1000 }, function(t) {
            var map = new mbgl.Map(options);

            t.doesNotThrow(function() {
                map.load(style);
            });

            map.release();
            t.end();
        });

        t.test('accepts a stringified stylesheet', { timeout: 1000 }, function(t) {
            var map = new mbgl.Map(options);

            t.doesNotThrow(function() {
                map.load(JSON.stringify(style));
            });

            map.release();
            t.end();
        });

        t.test('does not immediately trigger any tile loads', function(t) {
            var map = new mbgl.Map({
                request: function(req) {
                    t.fail('unexpected request ' + req.url);
                },
                ratio: 1
            });

            map.load(style);

            setTimeout(function() {
                map.release();
                t.end();
            }, 100);
        });
    });

    t.test('.render', function(t) {
        var options = {
            request: function(req, callback) {
                fs.readFile(path.join(__dirname, '..', req.url), function(err, data) {
                    callback(err, { data: data });
                });
            },
            ratio: 1
        };

        t.test('requires an object as the first parameter', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.render();
            }, /First argument must be an options object/);

            t.throws(function() {
                map.render('invalid');
            }, /First argument must be an options object/);

            map.release();
            t.end();
        });

        t.test('requires a callback as the second parameter', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.render({});
            }, /Second argument must be a callback function/);

            t.throws(function() {
                map.render({}, 'invalid');
            }, /Second argument must be a callback function/);

            map.release();
            t.end();
        });

        t.test('requires a style to be set', function(t) {
            var map = new mbgl.Map(options);

            t.throws(function() {
                map.render({}, function() {});
            }, /Style is not loaded/);

            map.release();
            t.end();
        });

        t.test('returns an error delayed', function(t) {
            var delay = 0;
            var map = new mbgl.Map({
                request: function(req, callback) {
                    delay += 100;
                    setTimeout(function() {
                        callback(new Error('not found'));
                    }, delay);
                },
                ratio: 1
            });
            map.load(style);
            map.render({ zoom: 1 }, function(err, data) {
                map.release();

                t.ok(err, 'returns error');
                t.end();
            });
        });

        t.test('returns an error', function(t) {
            var map = new mbgl.Map(options);
            map.load(style);
            map.render({ zoom: 1 }, function(err, data) {
                map.release();

                t.ok(err, 'returns error');
                t.end();
            });
        });

        t.test('double release', function(t) {
            var map = new mbgl.Map(options);
            map.release();

            t.throws(function() {
                map.release();
            }, /Map resources have already been released/);

            t.end();
        });

        t.test('returns an image', function(t) {
            var map = new mbgl.Map(options);
            map.load(style);
            map.render({}, function(err, pixels) {
                t.error(err);
                map.release();
                t.ok(pixels);
                t.ok(pixels instanceof Buffer);
                t.equal(pixels.length, 512 * 512 * 4)
                t.end();
            });
        });

        t.test('can be called several times in serial', function(t) {
            var completed = 0;
            var remaining = 10;
            var start = +new Date;

            var map = new mbgl.Map(options);
            map.load(style);

            function render() {
                map.render({}, function(err, data) {
                    t.error(err);

                    t.ok(true, 'render @ ' + ((+new Date) - start) + 'ms');
                    if (++completed === remaining) {
                        map.release();
                        t.end();
                    } else {
                        render();
                    }
                });
            }

            render();
        });

        t.test('throws if called in parallel', function(t) {
            var map = new mbgl.Map(options);
            map.load(style);

            t.throws(function() {
                map.render({}, function() {});
                map.render({}, function() {});
            }, /Map is currently rendering an image/);

            map.release();
            t.end();
        });

        // This can't be tested with a test-suite render test because zoom and center
        // are set via a different code path when included as style properties.
        t.test('sets zoom before center', function(t) {
            var map = new mbgl.Map(options);
            map.load({
                "version": 8,
                "sources": {
                    "geojson": {
                        "type": "geojson",
                        "data": {
                            "type": "Point",
                            "coordinates": [
                                18.05489,
                                59.32744
                            ]
                        }
                    }
                },
                "layers": [
                    {
                        "id": "circle",
                        "type": "circle",
                        "source": "geojson",
                        "paint": {
                            "circle-color": "red"
                        }
                    }
                ]
            });
            map.render({width: 400, height: 400, zoom: 5, center: [18.05489, 59.32744]}, function(err, actual) {
                t.error(err);

                var PNG = require('pngjs').PNG;
                var pixelmatch = require('pixelmatch');
                var expected = PNG.sync.read(
                    fs.readFileSync(path.join(__dirname, '../fixtures/zoom-center/expected.png'))).data;
                var numPixels = pixelmatch(actual, expected, undefined, 400, 400, { threshold: 0.13 });
                t.equal(numPixels, 0);
                t.end();
            });
        })
    });

    t.test('request callback', function (t) {
        t.test('returning an error', function(t) {
            var map = new mbgl.Map({
                request: function(req, callback) {
                    setImmediate(function () {
                        callback(new Error('request error'));
                    });
                },
            });
            map.load(style);
            map.render({ zoom: 1 }, function(err, data) {
                map.release();
                t.ok(err, 'returns error');
                t.equal(err.message, 'request error');
                t.end();
            });
        });

        t.test('returning no content for a tile', function(t) {
            var map = new mbgl.Map({
                request: function(req, callback) {
                    callback();
                },
            });
            map.load(style);
            map.render({ zoom: 1 }, function(err, data) {
                map.release();
                t.ok(data, 'no error');
                t.end();
            });
        });

        t.test('not holding references', function(t) {
            var options = {
                request: function() {},
                ratio: 1
            };

            // We explicitly don't call release. mbgl.Map should
            // not hold any reference to the node's main loop and
            // prevent the test from exit.
            var map1 = new mbgl.Map(options);
            var map2 = new mbgl.Map(options);
            var map3 = new mbgl.Map(options);

            t.end();
        });
    });
});
