import { select as d3_select } from 'd3-selection';

describe('iD.rendererFeatures', function() {
    var dimensions = [1000, 1000];
    var context, features;

    beforeEach(function() {
        context = iD.coreContext().assetPath('../dist/').init();
        d3_select(document.createElement('div'))
            .attr('class', 'main-map')
            .call(context.map());
        context.map().zoom(16);
        features = iD.rendererFeatures(context);
    });

    describe('#keys', function() {
        it('returns feature keys', function() {
            var keys = features.keys();
            expect(keys).toContain(
                'points', 'traffic_roads', 'service_roads', 'paths',
                'buildings', 'landuse', 'boundaries', 'water', 'rail',
                'power', 'past_future', 'others'
            );
        });
    });

    describe('#disable', function() {
        it('disables features', function() {
            features.disable('water');
            expect(features.disabled()).toContain('water');
            expect(features.enabled()).not.toContain('water');
        });
    });

    describe('#enable', function() {
        it('enables features', function() {
            features.disable('water');
            features.enable('water');
            expect(features.disabled()).not.toContain('water');
            expect(features.enabled()).toContain('water');
        });
    });

    describe('#toggle', function() {
        it('toggles features', function() {
            features.toggle('water');
            expect(features.disabled()).toContain('water');
            expect(features.enabled()).not.toContain('water');

            features.toggle('water');
            expect(features.disabled()).not.toContain('water');
            expect(features.enabled()).toContain('water');
        });
    });

    describe('#gatherStats', function() {
        it('counts features', function() {
            var graph = new iD.coreGraph([
                new iD.osmNode({id: 'point_bar', tags: {amenity: 'bar'}, version: 1}),
                new iD.osmNode({id: 'point_dock', tags: {waterway: 'dock'}, version: 1}),
                new iD.osmNode({id: 'point_rail_station', tags: {railway: 'station'}, version: 1}),
                new iD.osmNode({id: 'point_generator', tags: {power: 'generator'}, version: 1}),
                new iD.osmNode({id: 'point_old_rail_station', tags: {'disused:railway': 'station'}, version: 1}),
                new iD.osmWay({id: 'motorway', tags: {highway: 'motorway'}, version: 1}),
                new iD.osmWay({id: 'building_yes', tags: {area: 'yes', amenity: 'school', building: 'yes'}, version: 1}),
                new iD.osmWay({id: 'boundary', tags: {boundary: 'administrative'}, version: 1}),
                new iD.osmWay({id: 'fence', tags: {barrier: 'fence'}, version: 1})
            ]);
            var all = Object.values(graph.base().entities);
            var stats;

            features.gatherStats(all, graph, dimensions);
            stats = features.stats();

            expect(stats.boundaries).toEqual(1);
            expect(stats.buildings).toEqual(1);
            expect(stats.landuse).toEqual(0);
            expect(stats.traffic_roads).toEqual(1);
            expect(stats.service_roads).toEqual(0);
            expect(stats.others).toEqual(1);
            expect(stats.past_future).toEqual(1);
            expect(stats.paths).toEqual(0);
            expect(stats.points).toEqual(5);
            expect(stats.power).toEqual(1);
            expect(stats.rail).toEqual(1);
            expect(stats.water).toEqual(1);
        });
    });

    describe('matching', function() {
        var graph = new iD.coreGraph([
            // Points
            new iD.osmNode({id: 'point_bar', tags: {amenity: 'bar'}, version: 1}),
            new iD.osmNode({id: 'point_dock', tags: {waterway: 'dock'}, version: 1}),
            new iD.osmNode({id: 'point_rail_station', tags: {railway: 'station'}, version: 1}),
            new iD.osmNode({id: 'point_generator', tags: {power: 'generator'}, version: 1}),
            new iD.osmNode({id: 'point_old_rail_station', tags: {'disused:railway': 'station'}, version: 1}),

            // Traffic Roads
            new iD.osmWay({id: 'motorway', tags: {highway: 'motorway'}, version: 1}),
            new iD.osmWay({id: 'motorway_link', tags: {highway: 'motorway_link'}, version: 1}),
            new iD.osmWay({id: 'trunk', tags: {highway: 'trunk'}, version: 1}),
            new iD.osmWay({id: 'trunk_link', tags: {highway: 'trunk_link'}, version: 1}),
            new iD.osmWay({id: 'primary', tags: {highway: 'primary'}, version: 1}),
            new iD.osmWay({id: 'primary_link', tags: {highway: 'primary_link'}, version: 1}),
            new iD.osmWay({id: 'secondary', tags: {highway: 'secondary'}, version: 1}),
            new iD.osmWay({id: 'secondary_link', tags: {highway: 'secondary_link'}, version: 1}),
            new iD.osmWay({id: 'tertiary', tags: {highway: 'tertiary'}, version: 1}),
            new iD.osmWay({id: 'tertiary_link', tags: {highway: 'tertiary_link'}, version: 1}),
            new iD.osmWay({id: 'residential', tags: {highway: 'residential'}, version: 1}),
            new iD.osmWay({id: 'unclassified', tags: {highway: 'unclassified'}, version: 1}),
            new iD.osmWay({id: 'living_street', tags: {highway: 'living_street'}, version: 1}),

            // Service Roads
            new iD.osmWay({id: 'service', tags: {highway: 'service'}, version: 1}),
            new iD.osmWay({id: 'road', tags: {highway: 'road'}, version: 1}),
            new iD.osmWay({id: 'track', tags: {highway: 'track'}, version: 1}),

            // Paths
            new iD.osmWay({id: 'path', tags: {highway: 'path'}, version: 1}),
            new iD.osmWay({id: 'footway', tags: {highway: 'footway'}, version: 1}),
            new iD.osmWay({id: 'cycleway', tags: {highway: 'cycleway'}, version: 1}),
            new iD.osmWay({id: 'bridleway', tags: {highway: 'bridleway'}, version: 1}),
            new iD.osmWay({id: 'steps', tags: {highway: 'steps'}, version: 1}),
            new iD.osmWay({id: 'pedestrian', tags: {highway: 'pedestrian'}, version: 1}),

            // Buildings
            new iD.osmWay({id: 'building_yes', tags: {area: 'yes', amenity: 'school', building: 'yes'}, version: 1}),
            new iD.osmWay({id: 'building_no', tags: {area: 'yes', amenity: 'school', building: 'no'}, version: 1}),
            new iD.osmWay({id: 'building_part', tags: { 'building:part': 'yes'}, version: 1}),
            new iD.osmWay({id: 'garage1', tags: {area: 'yes', amenity: 'parking', parking: 'multi-storey'}, version: 1}),
            new iD.osmWay({id: 'garage2', tags: {area: 'yes', amenity: 'parking', parking: 'sheds'}, version: 1}),
            new iD.osmWay({id: 'garage3', tags: {area: 'yes', amenity: 'parking', parking: 'carports'}, version: 1}),
            new iD.osmWay({id: 'garage4', tags: {area: 'yes', amenity: 'parking', parking: 'garage_boxes'}, version: 1}),
            new iD.osmWay({id: 'building_construction', tags: {building: 'construction'}, version: 1}),

            // Indoor
            new iD.osmWay({id: 'room', tags: {area: 'yes', indoor: 'room'}, version: 1}),
            new iD.osmWay({id: 'indoor_area', tags: {area: 'yes', indoor: 'area'}, version: 1}),
            new iD.osmWay({id: 'indoor_bar', tags: {area: 'yes', indoor: 'room', amenity: 'bar'}, version: 1}),
            new iD.osmWay({id: 'corridor', tags: {highway: 'corridor', indoor: 'yes'}, version: 1}),

            // Pistes
            new iD.osmWay({id: 'downhill_piste', tags: {'piste:type': 'downhill'}, version: 1}),
            new iD.osmWay({id: 'piste_track_combo', tags: {'piste:type': 'alpine', highway: 'track'}, version: 1}),

            // Climbing routes
            new iD.osmWay({id: 'climbing_route', tags: {'climbing': 'route'}, version: 1}),

            // Aerialways
            new iD.osmWay({id: 'gondola', tags: {aerialway: 'gondola'}, version: 1}),
            new iD.osmWay({id: 'zip_line', tags: {aerialway: 'zip_line'}, version: 1}),
            new iD.osmWay({id: 'aerialway_platform', tags: {public_transport: 'platform', aerialway: 'yes'}, version: 1}),
            new iD.osmWay({id: 'old_aerialway_station', tags: {area: 'yes', aerialway: 'station'}, version: 1}),

            // Landuse
            new iD.osmWay({id: 'forest', tags: {area: 'yes', landuse: 'forest'}, version: 1}),
            new iD.osmWay({id: 'scrub', tags: {area: 'yes', natural: 'scrub'}, version: 1}),
            new iD.osmWay({id: 'industrial', tags: {area: 'yes', landuse: 'industrial'}, version: 1}),
            new iD.osmWay({id: 'parkinglot', tags: {area: 'yes', amenity: 'parking', parking: 'surface'}, version: 1}),
            new iD.osmWay({id: 'park', tags: {area: 'yes', leisure: 'park', parking: 'surface'}, version: 1}),

            // Landuse Multipolygon
            new iD.osmWay({id: 'outer', version: 1}),
            new iD.osmWay({id: 'inner1', version: 1}),
            new iD.osmWay({id: 'inner2', tags: {barrier: 'fence'}, version: 1}),
            new iD.osmWay({id: 'inner3', tags: {highway: 'residential'}, version: 1}),
            new iD.osmRelation({id: 'retail', tags: {landuse: 'retail', type: 'multipolygon'},
                    members: [
                        {id: 'outer', role: 'outer', type: 'way'},
                        {id: 'inner1', role: 'inner', type: 'way'},
                        {id: 'inner2', role: 'inner', type: 'way'},
                        {id: 'inner3', role: 'inner', type: 'way'}
                    ],
                    version: 1
                }),

            // Boundaries
            new iD.osmWay({id: 'boundary', tags: {boundary: 'administrative'}, version: 1}),
            new iD.osmWay({id: 'boundary_road', tags: {boundary: 'administrative', highway: 'primary'}, version: 1}),

            new iD.osmWay({id: 'boundary_member', version: 1}),
            new iD.osmWay({id: 'boundary_member2', version: 1}),

            // Boundary relations
            new iD.osmRelation({id: 'boundary_relation', tags: {type: 'boundary', boundary: 'administrative'},
                    members: [
                        {id: 'boundary_member'},
                    ],
                    version: 1
                }),
            new iD.osmRelation({id: 'boundary_relation2', tags: {type: 'boundary', boundary: 'administrative'},
                    members: [
                        // ways can be members of multiple boundary relations
                        {id: 'boundary_member'},
                        {id: 'boundary_member2'}
                    ],
                    version: 1
                }),

            // Water
            new iD.osmWay({id: 'water', tags: {area: 'yes', natural: 'water'}, version: 1}),
            new iD.osmWay({id: 'coastline', tags: {natural: 'coastline'}, version: 1}),
            new iD.osmWay({id: 'bay', tags: {area: 'yes', natural: 'bay'}, version: 1}),
            new iD.osmWay({id: 'pond', tags: {area: 'yes', landuse: 'pond'}, version: 1}),
            new iD.osmWay({id: 'basin', tags: {area: 'yes', landuse: 'basin'}, version: 1}),
            new iD.osmWay({id: 'reservoir', tags: {area: 'yes', landuse: 'reservoir'}, version: 1}),
            new iD.osmWay({id: 'salt_pond', tags: {area: 'yes', landuse: 'salt_pond'}, version: 1}),
            new iD.osmWay({id: 'river', tags: {waterway: 'river'}, version: 1}),

            // Rail
            new iD.osmWay({id: 'railway', tags: {railway: 'rail'}, version: 1}),
            new iD.osmWay({id: 'rail_landuse', tags: {area: 'yes', landuse: 'railway'}, version: 1}),
            new iD.osmWay({id: 'rail_disused', tags: {railway: 'disused'}, version: 1}),
            new iD.osmWay({id: 'rail_streetcar', tags: {railway: 'tram', highway: 'residential'}, version: 1}),
            new iD.osmWay({id: 'rail_trail', tags: {railway: 'disused', highway: 'cycleway'}, version: 1}),

            // Power
            new iD.osmWay({id: 'power_line', tags: {power: 'line'}, version: 1}),

            // Past/Future
            new iD.osmWay({id: 'motorway_construction', tags: {highway: 'construction', construction: 'motorway'}, version: 1}),
            new iD.osmWay({id: 'cycleway_proposed', tags: {highway: 'proposed', proposed: 'cycleway'}, version: 1}),
            new iD.osmWay({id: 'landuse_construction', tags: {area: 'yes', landuse: 'construction'}, version: 1}),

            // Others
            new iD.osmWay({id: 'fence', tags: {barrier: 'fence'}, version: 1}),
            new iD.osmWay({id: 'pipeline', tags: {man_made: 'pipeline'}, version: 1}),

            // Site relation
            new iD.osmRelation({id: 'site', tags: {type: 'site'},
                    members: [
                        {id: 'fence', role: 'perimeter'},
                        {id: 'building_yes'}
                    ],
                    version: 1
                })

        ]);
        var all = Object.values(graph.base().entities);


        function doMatch(rule, ids) {
            ids.forEach(function(id) {
                var entity = graph.entity(id);
                var geometry = entity.geometry(graph);
                const matches = features.getMatches(entity, graph, geometry);
                expect(matches).toHaveProperty(rule);
            });
        }

        function dontMatch(rule, ids) {
            ids.forEach(function(id) {
                var entity = graph.entity(id);
                var geometry = entity.geometry(graph);
                const matches = features.getMatches(entity, graph, geometry);
                expect(matches).not.toHaveProperty(rule);
            });
        }


        it('matches points', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('points', [
                'point_bar', 'point_dock', 'point_rail_station',
                'point_generator', 'point_old_rail_station'
            ]);

            dontMatch('points', [
                'motorway', 'service', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches traffic roads', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('traffic_roads', [
                'motorway', 'motorway_link', 'trunk', 'trunk_link',
                'primary', 'primary_link', 'secondary', 'secondary_link',
                'tertiary', 'tertiary_link', 'residential', 'living_street',
                'unclassified', 'boundary_road', 'inner3'
            ]);

            dontMatch('traffic_roads', [
                'point_bar', 'service', 'road', 'track', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches service roads', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('service_roads', [
                'service', 'road', 'track', 'piste_track_combo'
            ]);

            dontMatch('service_roads', [
                'point_bar', 'motorway', 'unclassified', 'living_street',
                'path', 'building_yes', 'forest', 'boundary', 'boundary_member', 'water',
                'railway', 'power_line', 'motorway_construction', 'fence'
            ]);
        });


        it('matches paths', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('paths', [
                'path', 'footway', 'cycleway', 'bridleway',
                'steps', 'pedestrian'
            ]);

            dontMatch('paths', [
                'point_bar', 'motorway', 'service', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence', 'corridor'
            ]);
        });


        it('matches buildings', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('buildings', [
                'building_yes',
                'garage1', 'garage2', 'garage3', 'garage4'
            ]);

            dontMatch('buildings', [
                'building_no', 'building_construction', 'point_bar', 'motorway', 'service', 'path',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches building_parts', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('building_parts', [
                'building_part'
            ]);

            dontMatch('building_parts', [
                'building_yes',
                'garage1', 'garage2', 'garage3', 'garage4',
                'building_no', 'point_bar', 'motorway', 'service', 'path',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches indoor', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('indoor', [
                'room', 'indoor_area', 'indoor_bar', 'corridor'
            ]);

            dontMatch('indoor', [
                'downhill_piste', 'piste_track_combo',
                'building_part', 'garage1', 'garage2', 'garage3', 'garage4',
                'building_no', 'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence',
                'inner3', 'forest', 'scrub', 'industrial', 'parkinglot', 'building_no',
                'rail_landuse', 'landuse_construction', 'retail',
                'outer', 'inner1', 'inner2'
            ]);
        });


        it('matches pistes', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('pistes', [
                'downhill_piste', 'piste_track_combo'
            ]);

            dontMatch('pistes', [
                'room', 'indoor_area', 'indoor_bar', 'corridor',
                'building_part', 'garage1', 'garage2', 'garage3', 'garage4',
                'building_no', 'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence',
                'inner3', 'forest', 'scrub', 'industrial', 'parkinglot', 'building_no',
                'rail_landuse', 'landuse_construction', 'retail',
                'outer', 'inner1', 'inner2'
            ]);
        });


        it('matches aerialways', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('aerialways', [
                'gondola', 'zip_line'
            ]);

            dontMatch('aerialways', [
                'aerialway_platform', 'old_aerialway_station',

                'downhill_piste', 'piste_track_combo',
                'room', 'indoor_area', 'indoor_bar', 'corridor',
                'building_part', 'garage1', 'garage2', 'garage3', 'garage4',
                'building_no', 'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence',
                'inner3', 'forest', 'scrub', 'industrial', 'parkinglot', 'building_no',
                'rail_landuse', 'landuse_construction', 'retail',
                'outer', 'inner1', 'inner2'
            ]);
        });


        it('matches landuse', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('landuse', [
                'forest', 'scrub', 'industrial', 'parkinglot', 'building_no',
                'rail_landuse', 'landuse_construction', 'retail',
                'outer', 'inner1', 'inner2'  // non-interesting members of landuse multipolygon
            ]);

            dontMatch('landuse', [
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence',
                'inner3'   // member of landuse multipolygon, but tagged as highway
            ]);
        });


        it('matches boundaries', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('boundaries', [
                'boundary',
                // match ways that are part of boundary relations - #5601
                'boundary_member', 'boundary_member2',
                // relations
                'boundary_relation', 'boundary_relation2'
            ]);

            dontMatch('boundaries', [
                'boundary_road',   // because boundary also used as highway - #4973
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'forest', 'water', 'railway', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches water', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('water', [
                'point_dock', 'water', 'coastline', 'bay', 'pond',
                'basin', 'reservoir', 'salt_pond', 'river'
            ]);

            dontMatch('water', [
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'railway', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches rail', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('rail', [
                'point_rail_station', 'railway', 'rail_landuse'
            ]);

            dontMatch('rail', [
                'rail_streetcar', 'rail_trail',  // because rail also used as highway
                'point_old_rail_station',
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'power_line',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches power', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('power', [
                'point_generator', 'power_line'
            ]);

            dontMatch('power', [
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'railway',
                'motorway_construction', 'fence'
            ]);
        });


        it('matches past/future', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('past_future', [
                'building_construction', 'point_old_rail_station', 'rail_disused',
                'motorway_construction', 'cycleway_proposed', 'landuse_construction'
            ]);

            dontMatch('past_future', [
                'rail_trail',  // because rail also used as highway
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line', 'fence'
            ]);
        });


        it('matches others', function () {
            features.gatherStats(all, graph, dimensions);

            doMatch('others', [
                'fence', 'pipeline'
            ]);

            dontMatch('others', [
                'point_bar', 'motorway', 'service', 'path', 'building_yes',
                'forest', 'boundary', 'boundary_member', 'water', 'railway', 'power_line',
                'motorway_construction', 'retail', 'outer', 'inner1', 'inner2', 'inner3'
            ]);
        });
    });


    describe('hiding', function() {
        it('hides child vertices on a hidden way', function() {
            var a = new iD.osmNode({id: 'a', version: 1});
            var b = new iD.osmNode({id: 'b', version: 1});
            var w = new iD.osmWay({id: 'w', nodes: [a.id, b.id], tags: {highway: 'path'}, version: 1});
            var graph = new iD.coreGraph([a, b, w]);
            var geometry = a.geometry(graph);
            var all = Object.values(graph.base().entities);

            features.disable('paths');
            features.gatherStats(all, graph, dimensions);

            expect(features.isHiddenChild(a, graph, geometry)).toBe(true);
            expect(features.isHiddenChild(b, graph, geometry)).toBe(true);
            expect(features.isHidden(a, graph, geometry)).toBe(true);
            expect(features.isHidden(b, graph, geometry)).toBe(true);
        });

        it('hides uninteresting (e.g. untagged or "other") member ways on a hidden multipolygon relation', function() {
            var outer = new iD.osmWay({id: 'outer', tags: {}, version: 1});
            var inner1 = new iD.osmWay({id: 'inner1', tags: {barrier: 'fence'}, version: 1});
            var inner2 = new iD.osmWay({id: 'inner2', version: 1});
            var inner3 = new iD.osmWay({id: 'inner3', tags: {highway: 'residential'}, version: 1});
            var r = new iD.osmRelation({
                id: 'r',
                tags: {type: 'multipolygon', natural: 'wood'},
                members: [
                    {id: outer.id, role: 'outer', type: 'way'},
                    {id: inner1.id, role: 'inner', type: 'way'},
                    {id: inner2.id, role: 'inner', type: 'way'},
                    {id: inner3.id, role: 'inner', type: 'way'}
                ],
                version: 1
            });
            var graph = new iD.coreGraph([outer, inner1, inner2, inner3, r]);
            var all = Object.values(graph.base().entities);

            features.disable('landuse');
            features.gatherStats(all, graph, dimensions);

            expect(features.isHidden(outer, graph, outer.geometry(graph))).toBe(true);     // #2548
            expect(features.isHidden(inner1, graph, inner1.geometry(graph))).toBe(true);   // #2548
            expect(features.isHidden(inner2, graph, inner2.geometry(graph))).toBe(true);   // #2548
            expect(features.isHidden(inner3, graph, inner3.geometry(graph))).toBe(false);  // #2887
        });

        it('hides only versioned entities', function() {
            var a = new iD.osmNode({id: 'a', version: 1});
            var b = new iD.osmNode({id: 'b'});
            var graph = new iD.coreGraph([a, b]);
            var ageo = a.geometry(graph);
            var bgeo = b.geometry(graph);
            var all = Object.values(graph.base().entities);

            features.disable('points');
            features.gatherStats(all, graph, dimensions);

            expect(features.isHidden(a, graph, ageo)).toBe(true);
            expect(features.isHidden(b, graph, bgeo)).toBe(false);
        });

        it('#forceVisible', function() {
            var a = new iD.osmNode({id: 'a', version: 1});
            var graph = new iD.coreGraph([a]);
            var ageo = a.geometry(graph);
            var all = Object.values(graph.base().entities);

            features.disable('points');
            features.gatherStats(all, graph, dimensions);
            features.forceVisible(['a']);

            expect(features.isHidden(a, graph, ageo)).toBe(false);
        });

        it('auto-hides features', function() {
            var graph = new iD.coreGraph([]);
            var maxPoints = 200;
            var all, hidden, autoHidden, i, msg;

            for (i = 0; i < maxPoints; i++) {
                graph.rebase([new iD.osmNode({version: 1})], [graph]);
            }

            all = Object.values(graph.base().entities);
            features.gatherStats(all, graph, dimensions);
            hidden = features.hidden();
            autoHidden = features.autoHidden();
            msg = i + ' points';

            expect(hidden, msg).not.toContain('points');
            expect(autoHidden, msg).not.toContain('points');

            graph.rebase([new iD.osmNode({version: 1})], [graph]);

            all = Object.values(graph.base().entities);
            features.gatherStats(all, graph, dimensions);
            hidden = features.hidden();
            autoHidden = features.autoHidden();
            msg = (i + 1) + ' points';

            expect(hidden, msg).toContain('points');
            expect(autoHidden, msg).toContain('points');
        });

        it('doubles auto-hide threshold when doubling viewport size', function() {
            var graph = new iD.coreGraph([]);
            var maxPoints = 400;
            var dimensions = [2000, 1000];
            var all, hidden, autoHidden, i, msg;

            for (i = 0; i < maxPoints; i++) {
                graph.rebase([new iD.osmNode({version: 1})], [graph]);
            }

            all = Object.values(graph.base().entities);
            features.gatherStats(all, graph, dimensions);
            hidden = features.hidden();
            autoHidden = features.autoHidden();
            msg = i + ' points';

            expect(hidden, msg).not.toContain('points');
            expect(autoHidden, msg).not.toContain('points');

            graph.rebase([new iD.osmNode({version: 1})], [graph]);

            all = Object.values(graph.base().entities);
            features.gatherStats(all, graph, dimensions);
            hidden = features.hidden();
            autoHidden = features.autoHidden();
            msg = (i + 1) + ' points';

            expect(hidden, msg).toContain('points');
            expect(autoHidden, msg).toContain('points');
        });
    });

});
