'use strict';

/**
 * ET-006 — поведенческие unit-тесты модуля загрузки GPX-треков.
 *
 * Покрывают группы unit-кейсов из docs/work-items/ET-006/04-test-plan.yaml:
 *   - unit-gpx-parser  (U-01..U-08)
 *   - unit-gpx-stats   (U-10..U-14)
 *   - unit-gpx-colors  (U-20, U-21)
 * плюс чистые функции построения GeoJSON и bbox, плюс регрессии по
 * замечаниям код-ревью ET-006: P1-1 (большие треки не валят расчёт
 * статистики), P2-1 (агрегация статистики и профиля по всем трекам
 * файла), P2-2 (чанковый расчёт статистики — trackStatsChunked).
 *
 * Тесты исполняют РЕАЛЬНЫЙ модуль src/web/gpx.js. Браузерный примитив
 * `DOMParser` (ADR-003) в Node отсутствует, поэтому подставляется
 * компактный мок-парсер XML (`MockDOMParser`) — он генерирует DOM-lite
 * узлы с тем подмножеством DOM API, которое использует gpx.js
 * (`getElementsByTagName`, `getAttribute`, `textContent`). Это позволяет
 * проверить настоящую GPX-семантику конвертации, не таща в проект jsdom.
 *
 * Две поправки к числовым ПРИМЕРАМ в 04-test-plan.yaml (сам ТЗ §5
 * корректен, расходятся лишь оценочные числа аналитика в примерах):
 *   - U-10: для точек 0.1°×0.1° каноническая формула Haversine (та же,
 *     что в app.js) даёт ≈25.5 км, а не 28.3 км. Тест проверяет 25.5.
 *   - U-11: для ele [100,150,120,200,180] сброс высоты = 30+20 = 50 м
 *     (как и записано в самой расшифровке кейса), а не 70 м.
 *
 * Запуск: `node --test tests/unit/gpx.test.js`
 * (в CI оборачивается pytest-тестом tests/unit/test_gpx_upload.py).
 */

const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');

const GPX_PATH = path.join(__dirname, '..', '..', 'src', 'web', 'gpx.js');

// ─── Мини-XML-парсер: DOM-lite для подмены браузерного DOMParser ────────────

/** Декодирует базовые XML-сущности. */
function decodeEntities(s) {
  return String(s).replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, ent) => {
    if (ent[0] === '#') {
      const hex = ent[1] === 'x' || ent[1] === 'X';
      const code = hex ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
      return isNaN(code) ? m : String.fromCodePoint(code);
    }
    const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
    return ent in named ? named[ent] : m;
  });
}

/** Локальное имя тега (без префикса namespace). */
function localName(tag) {
  const i = tag.indexOf(':');
  return i === -1 ? tag : tag.slice(i + 1);
}

/** DOM-lite элемент. */
class El {
  constructor(tagName) {
    this.tagName = tagName;
    this.nodeName = tagName;
    this._attrs = {};
    this.childNodes = [];
  }

  getAttribute(name) {
    return name in this._attrs ? this._attrs[name] : null;
  }

  getElementsByTagName(name) {
    const out = [];
    const walk = (node) => {
      node.childNodes.forEach((c) => {
        if (c instanceof El) {
          if (c.tagName === name || localName(c.tagName) === name) out.push(c);
          walk(c);
        }
      });
    };
    walk(this);
    return out;
  }

  get textContent() {
    return this.childNodes
      .map((c) => (c instanceof El ? c.textContent : c.text))
      .join('');
  }
}

/** DOM-lite документ. */
class Doc {
  constructor(root) {
    this.documentElement = root;
  }

  getElementsByTagName(name) {
    const root = this.documentElement;
    if (!root) return [];
    const out = [];
    if (root.tagName === name || localName(root.tagName) === name) out.push(root);
    return out.concat(root.getElementsByTagName(name));
  }
}

/** Разбирает строку XML в DOM-lite дерево. Бросает Error на невалидном XML. */
function parseXml(input) {
  let s = String(input).replace(/^﻿/, '');
  s = s.replace(/<\?[\s\S]*?\?>/g, '');
  s = s.replace(/<!--[\s\S]*?-->/g, '');
  s = s.replace(/<!DOCTYPE[\s\S]*?>/gi, '');
  s = s.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g,
    (m, c) => c.replace(/&/g, '&amp;').replace(/</g, '&lt;'));

  const stack = [];
  let root = null;
  let i = 0;

  while (i < s.length) {
    const lt = s.indexOf('<', i);
    if (lt === -1) break;
    if (lt > i) {
      const text = s.slice(i, lt);
      if (stack.length && text.trim() !== '') {
        stack[stack.length - 1].childNodes.push({ text: decodeEntities(text) });
      }
    }
    const gt = s.indexOf('>', lt);
    if (gt === -1) throw new Error('malformed: no closing >');
    let tag = s.slice(lt + 1, gt).trim();
    i = gt + 1;

    if (tag[0] === '/') {
      const cname = tag.slice(1).trim();
      if (!stack.length || stack[stack.length - 1].tagName !== cname) {
        throw new Error('malformed: unbalanced tag ' + cname);
      }
      stack.pop();
      continue;
    }

    let selfClose = false;
    if (tag[tag.length - 1] === '/') {
      selfClose = true;
      tag = tag.slice(0, -1).trim();
    }
    const m = tag.match(/^(\S+)([\s\S]*)$/);
    if (!m) throw new Error('malformed: empty tag');
    const el = new El(m[1]);
    const attrRe = /([^\s=]+)\s*=\s*"([^"]*)"|([^\s=]+)\s*=\s*'([^']*)'/g;
    let am;
    while ((am = attrRe.exec(m[2]))) {
      if (am[1] !== undefined) el._attrs[am[1]] = decodeEntities(am[2]);
      else el._attrs[am[3]] = decodeEntities(am[4]);
    }
    if (stack.length) stack[stack.length - 1].childNodes.push(el);
    else if (!root) root = el;
    else throw new Error('malformed: multiple roots');
    if (!selfClose) stack.push(el);
  }

  if (stack.length) throw new Error('malformed: unclosed tag');
  if (!root) throw new Error('malformed: no root element');
  return new Doc(root);
}

/**
 * Мок браузерного DOMParser. Как и настоящий — не бросает исключение,
 * а на невалидном XML возвращает документ с корнем `<parsererror>`.
 */
class MockDOMParser {
  parseFromString(str) {
    try {
      return parseXml(str);
    } catch (e) {
      const err = new El('parsererror');
      err.childNodes.push({ text: String(e.message) });
      return new Doc(err);
    }
  }
}

// ─── Загрузка модуля под тестом ────────────────────────────────────────────

global.DOMParser = MockDOMParser;
delete require.cache[require.resolve(GPX_PATH)];
const Gpx = require(GPX_PATH);

// ─── Генераторы тестовых GPX ───────────────────────────────────────────────

const NS = 'http://www.topografix.com/GPX/1/1';

/** Собирает GPX 1.1 с одним треком из списка точек {lat, lon, ele?, time?}. */
function gpxWithTrack(points, { name = 'Тест', xmlns = NS } = {}) {
  const pts = points.map((p) => {
    const ele = p.ele !== undefined ? `<ele>${p.ele}</ele>` : '';
    const time = p.time !== undefined ? `<time>${p.time}</time>` : '';
    return `<trkpt lat="${p.lat}" lon="${p.lon}">${ele}${time}</trkpt>`;
  }).join('');
  const ns = xmlns ? ` xmlns="${xmlns}"` : '';
  return `<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1"${ns}><trk><name>${name}</name><trkseg>${pts}</trkseg></trk></gpx>`;
}

/** 10 точек с ele и time — тест U-01. */
function tenPoints() {
  const pts = [];
  for (let i = 0; i < 10; i++) {
    pts.push({
      lat: 55.70 + i * 0.001,
      lon: 37.60 + i * 0.001,
      ele: 150 + i * 5,
      time: `2026-01-01T08:0${i}:00Z`,
    });
  }
  return pts;
}

// ─── unit-gpx-parser : U-01..U-08 ──────────────────────────────────────────

test('U-01: парсинг валидного GPX 1.1 с одним треком (10 точек)', () => {
  const model = Gpx.parseGpxText(gpxWithTrack(tenPoints()));
  assert.equal(model.tracks.length, 1);
  assert.equal(model.tracks[0].points.length, 10);
  // [lon, lat, ele, time]
  const first = model.tracks[0].points[0];
  assert.equal(first[0], 37.60);
  assert.equal(first[1], 55.70);
  assert.equal(first[2], 150);
  assert.equal(first[3], '2026-01-01T08:00:00Z');
  assert.equal(model.tracks[0].points[9][2], 195);
});

test('U-02: парсинг GPX с несколькими треками', () => {
  const trk = (n) => `<trk><name>T${n}</name><trkseg>` +
    `<trkpt lat="55.7" lon="37.6"/><trkpt lat="55.8" lon="37.7"/>` +
    '</trkseg></trk>';
  const xml = `<?xml version="1.0"?><gpx version="1.1" xmlns="${NS}">` +
    trk(1) + trk(2) + trk(3) + '</gpx>';
  const model = Gpx.parseGpxText(xml);
  assert.equal(model.tracks.length, 3);
  assert.deepEqual(model.tracks.map((t) => t.name), ['T1', 'T2', 'T3']);
});

test('U-03: парсинг waypoints (5 шт. с именами и координатами)', () => {
  let wpts = '';
  for (let i = 0; i < 5; i++) {
    wpts += `<wpt lat="${55.7 + i * 0.01}" lon="${37.6 + i * 0.01}">` +
      `<name>Точка ${i}</name><ele>${100 + i}</ele></wpt>`;
  }
  const xml = `<?xml version="1.0"?><gpx version="1.1" xmlns="${NS}">${wpts}</gpx>`;
  const model = Gpx.parseGpxText(xml);
  assert.equal(model.waypoints.length, 5);
  assert.equal(model.waypoints[0].name, 'Точка 0');
  assert.equal(model.waypoints[4].name, 'Точка 4');
  assert.equal(model.waypoints[2].ele, 102);
  assert.equal(model.waypoints[0].lon, 37.6);
});

test('U-04: парсинг route (rte) — трактуется как трек', () => {
  let rtepts = '';
  for (let i = 0; i < 20; i++) {
    rtepts += `<rtept lat="${55.7 + i * 0.001}" lon="${37.6 + i * 0.001}"/>`;
  }
  const xml = `<?xml version="1.0"?><gpx version="1.1" xmlns="${NS}">` +
    `<rte><name>Маршрут A</name>${rtepts}</rte></gpx>`;
  const model = Gpx.parseGpxText(xml);
  assert.equal(model.tracks.length, 1);
  assert.equal(model.tracks[0].points.length, 20);
  assert.equal(model.tracks[0].name, 'Маршрут A');
});

test('U-05: GPX без данных высот — ele=null, stats.elevGain=null', () => {
  const pts = [
    { lat: 55.70, lon: 37.60 },
    { lat: 55.71, lon: 37.61 },
    { lat: 55.72, lon: 37.62 },
  ];
  const model = Gpx.parseGpxText(gpxWithTrack(pts));
  const track = model.tracks[0];
  assert.equal(track.points[0][2], null);
  assert.equal(track.stats.elevGain, null);
  assert.equal(track.stats.elevLoss, null);
  assert.equal(track.stats.eleMin, null);
  assert.equal(track.stats.eleMax, null);
  assert.ok(track.stats.distanceKm > 0, 'длина считается и без высот');
});

test('U-06: невалидный XML — parseGpxText бросает PARSE_ERROR', () => {
  assert.throws(
    () => Gpx.parseGpxText('<gpx><trk><trkseg></trk></gpx>'),
    /PARSE_ERROR/,
  );
  assert.throws(
    () => Gpx.parseGpxText('это просто текст, а не XML'),
    /PARSE_ERROR/,
  );
});

test('U-07: пустой GPX (нет trk/wpt/rte) — бросает EMPTY', () => {
  const xml = `<?xml version="1.0"?><gpx version="1.1" xmlns="${NS}"></gpx>`;
  assert.throws(() => Gpx.parseGpxText(xml), /EMPTY/);
});

test('U-08: GPX без xmlns парсится корректно (fallback без namespace)', () => {
  const model = Gpx.parseGpxText(gpxWithTrack(tenPoints(), { xmlns: null }));
  assert.equal(model.tracks.length, 1);
  assert.equal(model.tracks[0].points.length, 10);
});

test('parseGpxAsync даёт тот же результат, что и синхронный парсер', async () => {
  const xml = gpxWithTrack(tenPoints());
  const sync = Gpx.parseGpxText(xml);
  const async = await Gpx.parseGpxAsync(xml);
  assert.deepEqual(async, sync);
});

test('parseGpxAsync отклоняется с EMPTY на пустом GPX', async () => {
  const xml = `<?xml version="1.0"?><gpx version="1.1" xmlns="${NS}"></gpx>`;
  await assert.rejects(Gpx.parseGpxAsync(xml), /EMPTY/);
});

test('extractGpxModel: трек и waypoints из одного файла', () => {
  const xml = `<?xml version="1.0"?><gpx version="1.1" xmlns="${NS}">` +
    '<trk><name>Tr</name><trkseg><trkpt lat="55.7" lon="37.6"/>' +
    '<trkpt lat="55.8" lon="37.7"/></trkseg></trk>' +
    '<wpt lat="55.75" lon="37.65"><name>Кафе</name></wpt></gpx>';
  const doc = new MockDOMParser().parseFromString(xml);
  const model = Gpx.extractGpxModel(doc);
  assert.equal(model.tracks.length, 1);
  assert.equal(model.waypoints.length, 1);
  assert.equal(model.waypoints[0].name, 'Кафе');
});

// ─── unit-gpx-stats : U-10..U-14 ───────────────────────────────────────────

test('U-10: длина трека по Haversine (каноническая формула проекта)', () => {
  const points = [
    [37.6, 55.7], [37.7, 55.8], [37.8, 55.9],
  ];
  const stats = Gpx.trackStats(points);
  // Каноническая Haversine (как в app.js haversineKm) для шага 0.1°×0.1°
  // даёт ≈25.5 км. Значение «28.3 км» в 04-test-plan.yaml — неточная
  // оценка аналитика; реализация следует ТЗ §5.1 (формула Haversine).
  assert.ok(
    Math.abs(stats.distanceKm - 25.5) < 0.5,
    `ожидали ≈25.5 км, получили ${stats.distanceKm}`,
  );
});

test('U-11: набор и сброс высоты по дельтам ele', () => {
  const points = [
    [37.6, 55.7, 100], [37.6, 55.71, 150], [37.6, 55.72, 120],
    [37.6, 55.73, 200], [37.6, 55.74, 180],
  ];
  const stats = Gpx.trackStats(points);
  // Дельты: +50, -30, +80, -20 → набор 130, сброс 50.
  assert.equal(stats.elevGain, 130);
  assert.equal(stats.elevLoss, 50);
});

test('U-12: фильтрация шума высот — дельты < 2 м игнорируются', () => {
  const points = [
    [37.6, 55.70, 100], [37.6, 55.71, 101], [37.6, 55.72, 100],
    [37.6, 55.73, 101], [37.6, 55.74, 150],
  ];
  const stats = Gpx.trackStats(points);
  // Колебания ±1 м не сдвигают опорную высоту → набор = 100→150 = 50 м.
  assert.equal(stats.elevGain, 50);
  assert.equal(stats.elevLoss, 0);
});

test('U-13: минимальная и максимальная высота', () => {
  const points = [
    [37.6, 55.70, 100], [37.6, 55.71, 250], [37.6, 55.72, 80],
    [37.6, 55.73, 300], [37.6, 55.74, 150],
  ];
  const stats = Gpx.trackStats(points);
  assert.equal(stats.eleMin, 80);
  assert.equal(stats.eleMax, 300);
});

test('U-14: статистика без данных высот — длина есть, высоты null', () => {
  const points = [
    [37.6, 55.70], [37.6, 55.71], [37.6, 55.72],
  ];
  const stats = Gpx.trackStats(points);
  assert.ok(stats.distanceKm > 0);
  assert.equal(stats.elevGain, null);
  assert.equal(stats.elevLoss, null);
  assert.equal(stats.eleMin, null);
  assert.equal(stats.eleMax, null);
});

test('trackStats: пустой трек — нулевая длина без падения', () => {
  const stats = Gpx.trackStats([]);
  assert.equal(stats.distanceKm, 0);
  assert.equal(stats.elevGain, null);
});

test('P1-1: trackStats не падает на треке с сотнями тысяч точек высот', () => {
  // Регрессия ревью P1-1: Math.min/max.apply на массиве такого размера
  // бросал RangeError: Maximum call stack size exceeded → файл не
  // загружался (нарушение REQ-NF-01). Однопроходный обход — без apply.
  const points = [];
  for (let i = 0; i < 500000; i++) {
    points.push([37.6 + i * 1e-6, 55.7 + i * 1e-6, 100 + (i % 50)]);
  }
  let stats;
  assert.doesNotThrow(() => { stats = Gpx.trackStats(points); });
  assert.equal(stats.eleMin, 100);
  assert.equal(stats.eleMax, 149);
  assert.ok(stats.distanceKm > 0, 'длина считается на большом треке');
});

test('trackStatsChunked даёт тот же результат, что и синхронный trackStats', async () => {
  const points = [
    [37.6, 55.70, 100], [37.6, 55.71, 150], [37.6, 55.72, 120],
    [37.6, 55.73, 200], [37.6, 55.74, 180],
  ];
  const chunked = await Gpx.trackStatsChunked(points);
  assert.deepEqual(chunked, Gpx.trackStats(points));
});

test('trackStatsChunked: расчёт верен на треке длиннее размера чанка', async () => {
  // > CHUNK_SIZE (8000) точек — статистика проходит через несколько чанков.
  const points = [];
  for (let i = 0; i < 20000; i++) {
    points.push([37.6 + i * 1e-5, 55.7, 100 + (i % 30)]);
  }
  const chunked = await Gpx.trackStatsChunked(points);
  assert.deepEqual(chunked, Gpx.trackStats(points));
});

// ─── unit-gpx-colors : U-20, U-21 ──────────────────────────────────────────

test('U-20: первый файл получает первый цвет палитры', () => {
  assert.equal(Gpx.colorForIndex(0), '#e6194b');
});

test('U-21: девятый файл получает первый цвет (цикл 8 % 8 = 0)', () => {
  assert.equal(Gpx.colorForIndex(8), '#e6194b');
  assert.equal(Gpx.colorForIndex(8), Gpx.colorForIndex(0));
});

test('палитра содержит ровно 8 цветов и отличается от цветов роутинга', () => {
  assert.equal(Gpx.PALETTE.length, 8);
  // Цвета роутинга из app.js — не должны пересекаться (TRZ REQ-F-04).
  const routeColors = ['#0066ff', '#00aa44', '#9933cc', '#ff8800', '#888888'];
  Gpx.PALETTE.forEach((c) => {
    assert.ok(!routeColors.includes(c), `${c} совпадает с цветом роутинга`);
  });
});

test('colorForIndex циклически проходит всю палитру', () => {
  for (let i = 0; i < 24; i++) {
    assert.equal(Gpx.colorForIndex(i), Gpx.PALETTE[i % 8]);
  }
});

// ─── Чистые функции: GeoJSON и bbox ────────────────────────────────────────

test('tracksToGeoJSON: трек → LineString-фича с [lon,lat]-координатами', () => {
  const tracks = [{ points: [[37.6, 55.7, 100], [37.7, 55.8, 110]] }];
  const fc = Gpx.tracksToGeoJSON(tracks);
  assert.equal(fc.type, 'FeatureCollection');
  assert.equal(fc.features.length, 1);
  assert.equal(fc.features[0].geometry.type, 'LineString');
  assert.deepEqual(fc.features[0].geometry.coordinates, [[37.6, 55.7], [37.7, 55.8]]);
});

test('waypointsToGeoJSON: waypoint → Point-фича с именем в properties', () => {
  const fc = Gpx.waypointsToGeoJSON([{ lon: 37.6, lat: 55.7, name: 'Брод' }]);
  assert.equal(fc.features.length, 1);
  assert.equal(fc.features[0].geometry.type, 'Point');
  assert.deepEqual(fc.features[0].geometry.coordinates, [37.6, 55.7]);
  assert.equal(fc.features[0].properties.name, 'Брод');
});

test('fileBounds: bbox охватывает все точки треков и waypoints', () => {
  const file = {
    tracks: [{ points: [[37.5, 55.6], [37.9, 55.9]] }],
    waypoints: [{ lon: 37.4, lat: 56.0 }],
  };
  const b = Gpx.fileBounds(file);
  assert.deepEqual(b, [[37.4, 55.6], [37.9, 56.0]]);
});

test('fileBounds: файл без точек → null', () => {
  assert.equal(Gpx.fileBounds({ tracks: [], waypoints: [] }), null);
});

// ─── Агрегация по файлу: aggregateStats / buildFileProfileSamples (P2-1) ────

test('P2-1: aggregateStats суммирует статистику всех треков файла', () => {
  // Ревью P2-1: панель показывает один файл, но файл может содержать
  // несколько <trk> — статистика должна охватывать их все, не только [0].
  const tracks = [
    { stats: { distanceKm: 10, elevGain: 100, elevLoss: 50, eleMin: 120, eleMax: 300 } },
    { stats: { distanceKm: 5, elevGain: 40, elevLoss: 20, eleMin: 90, eleMax: 250 } },
  ];
  const agg = Gpx.aggregateStats(tracks);
  assert.equal(agg.distanceKm, 15);
  assert.equal(agg.elevGain, 140);
  assert.equal(agg.elevLoss, 70);
  assert.equal(agg.eleMin, 90);
  assert.equal(agg.eleMax, 300);
});

test('P2-1: aggregateStats — трек без высот не ломает агрегацию', () => {
  const tracks = [
    { stats: { distanceKm: 10, elevGain: 100, elevLoss: 50, eleMin: 120, eleMax: 300 } },
    { stats: { distanceKm: 5, elevGain: null, elevLoss: null, eleMin: null, eleMax: null } },
  ];
  const agg = Gpx.aggregateStats(tracks);
  assert.equal(agg.distanceKm, 15);
  assert.equal(agg.elevGain, 100);
  assert.equal(agg.elevLoss, 50);
  assert.equal(agg.eleMin, 120);
  assert.equal(agg.eleMax, 300);
});

test('P2-1: aggregateStats — все треки без высот → поля высот null', () => {
  const tracks = [
    { stats: { distanceKm: 7, elevGain: null, elevLoss: null, eleMin: null, eleMax: null } },
  ];
  const agg = Gpx.aggregateStats(tracks);
  assert.equal(agg.distanceKm, 7);
  assert.equal(agg.elevGain, null);
  assert.equal(agg.elevLoss, null);
  assert.equal(agg.eleMin, null);
  assert.equal(agg.eleMax, null);
});

test('P2-1: buildFileProfileSamples объединяет высоты всех треков файла', () => {
  const t1 = { points: [[37.60, 55.70, 100], [37.61, 55.70, 200]] };
  const t2 = { points: [[37.70, 55.80, 300], [37.71, 55.80, 400]] };
  t1.stats = Gpx.trackStats(t1.points);
  t2.stats = Gpx.trackStats(t2.points);
  const samples = Gpx.buildFileProfileSamples({ tracks: [t1, t2] });
  // Все 4 точки с высотой попали в профиль — не только из tracks[0].
  assert.equal(samples.length, 4);
  assert.deepEqual(samples.map((s) => s.e), [100, 200, 300, 400]);
  // Расстояние — сквозное: второй трек смещён на длину первого.
  assert.equal(samples[0].d, 0);
  assert.ok(samples[2].d >= t1.stats.distanceKm - 1e-9);
  assert.ok(samples[3].d > samples[2].d);
});

// ─── Контракт модуля ───────────────────────────────────────────────────────

test('модуль публикует window.Gpx и onclick-обработчики', () => {
  assert.equal(global.Gpx, Gpx);
  assert.equal(typeof global.onGpxFileSelected, 'function');
  assert.equal(typeof global.toggleGpxSheet, 'function');
  assert.equal(typeof global.selectGpxTrack, 'function');
  assert.equal(typeof global.removeGpxTrack, 'function');
  assert.equal(typeof global.rebuildGpxOverlays, 'function');
});

test('MAX_FILE_BYTES равен 50 МБ (TRZ REQ-F-03)', () => {
  assert.equal(Gpx.MAX_FILE_BYTES, 50 * 1024 * 1024);
});
