'use strict';

/**
 * ET-005 — поведенческие unit-тесты модуля единиц измерения.
 *
 * Покрывают TP-01..TP-04 из docs/work-items/ET-005/04-test-plan.yaml и
 * критерии AC-2/AC-3 из 03-acceptance-criteria.md.
 *
 * Тесты исполняют РЕАЛЬНЫЙ модуль src/web/units.js: перед каждым тестом
 * подставляются мок-зависимости (window.localStorage, document) и модуль
 * загружается заново со сбросом кэша require — так гарантируется чистое
 * состояние (рантайм-кэш выбранной единицы внутри модуля).
 *
 * Запуск: `node --test tests/unit/units.test.js`
 * (в CI оборачивается pytest-тестом tests/unit/test_unit_toggle.py).
 */

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

const UNITS_PATH = path.join(__dirname, '..', '..', 'src', 'web', 'units.js');

/**
 * Готовит изолированное мок-окружение и загружает свежий модуль units.js.
 * @param {object} [opts]
 * @param {string} [opts.stored] - значение ключа distance_unit в localStorage
 *   (если не указан — ключ отсутствует).
 * @param {boolean} [opts.noStorage] - смоделировать недоступный localStorage.
 * @returns {{Units: object, events: object[], calls: object, store: object}}
 */
function loadUnits({ stored, noStorage = false } = {}) {
  const store = {};
  if (stored !== undefined) store['distance_unit'] = stored;

  const events = [];
  const calls = { setItem: [] };

  const localStorage = {
    getItem: (k) => (k in store ? store[k] : null),
    setItem: (k, v) => { store[k] = String(v); calls.setItem.push([k, String(v)]); },
  };

  global.window = noStorage
    ? { get localStorage() { throw new Error('localStorage disabled'); } }
    : { localStorage };
  global.document = {
    dispatchEvent: (ev) => { events.push(ev); return true; },
  };

  delete require.cache[require.resolve(UNITS_PATH)];
  const Units = require(UNITS_PATH);
  return { Units, events, calls, store };
}

// ── TP-01: единица по умолчанию — километры ─────────────────────────────
test('TP-01: без сохранённого значения getUnit() возвращает km', () => {
  const { Units } = loadUnits();
  assert.equal(Units.getUnit(), 'km');
});

test('TP-01: некорректное сохранённое значение даёт дефолт km', () => {
  const { Units } = loadUnits({ stored: 'parsec' });
  assert.equal(Units.getUnit(), 'km');
});

test('TP-01: в режиме km расстояния форматируются в км/м', () => {
  const { Units } = loadUnits();
  assert.equal(Units.formatDistance(12340), '12,3 км');
  assert.equal(Units.formatDistance(850), '850 м');
});

// ── TP-02: переключение на мили ─────────────────────────────────────────
test('TP-02: setUnit("mi") меняет единицу, пишет localStorage и шлёт событие', () => {
  const { Units, events, calls } = loadUnits();

  const result = Units.setUnit('mi');

  assert.equal(result, 'mi');
  assert.equal(Units.getUnit(), 'mi');
  assert.deepEqual(calls.setItem, [['distance_unit', 'mi']]);
  assert.equal(events.length, 1);
  assert.equal(events[0].type, 'unitchange');
  assert.equal(events[0].detail.unit, 'mi');
});

test('TP-02: в режиме mi расстояния пересчитываются в мили', () => {
  const { Units } = loadUnits();
  Units.setUnit('mi');
  // 100 км × 0.621371 = 62.1371 мили
  assert.equal(Units.formatDistance(100000), '62,1 ми');
  // карточки маршрутов запрашивают целочисленную точность (R5)
  assert.equal(Units.formatDistance(50000, { precision: 0 }), '31 ми');
});

test('TP-02: суб-километровые значения в режиме mi — с повышенной точностью (R2)', () => {
  const { Units } = loadUnits();
  Units.setUnit('mi');
  // 850 м × 0.621371 / 1000 = 0.528... → 2 знака, футы не вводятся
  assert.equal(Units.formatDistance(850), '0,53 ми');
});

// ── TP-03: персистентность выбора между загрузками страницы ─────────────
test('TP-03: сохранённое "mi" восстанавливается при следующей загрузке', () => {
  const { Units } = loadUnits({ stored: 'mi' });
  assert.equal(Units.getUnit(), 'mi');
});

test('TP-03: setUnit() сохраняет выбор в localStorage для будущих сессий', () => {
  const first = loadUnits();
  first.Units.setUnit('mi');
  assert.equal(first.store['distance_unit'], 'mi');

  // Имитация перезагрузки страницы: новый модуль с тем же хранилищем.
  const reloaded = loadUnits({ stored: first.store['distance_unit'] });
  assert.equal(reloaded.Units.getUnit(), 'mi');
});

// ── TP-04: обратное переключение на километры ───────────────────────────
test('TP-04: setUnit("km") из режима mi возвращает километры', () => {
  const { Units } = loadUnits({ stored: 'mi' });
  assert.equal(Units.getUnit(), 'mi');

  Units.setUnit('km');

  assert.equal(Units.getUnit(), 'km');
  assert.equal(Units.formatDistance(12340), '12,3 км');
});

test('TP-04: многократное переключение не накапливает ошибку округления', () => {
  const { Units } = loadUnits();
  const meters = 12340;
  const original = Units.formatDistance(meters);

  for (let i = 0; i < 20; i++) {
    Units.toggleUnit();
  }

  // 20 переключений = чётное число → снова km; значение не «дрейфует»,
  // потому что каноническое состояние всегда в метрах (ADR-0001 п.4).
  assert.equal(Units.getUnit(), 'km');
  assert.equal(Units.formatDistance(meters), original);
});

// ── toggleUnit() ────────────────────────────────────────────────────────
test('toggleUnit() переключает km ⇄ mi', () => {
  const { Units } = loadUnits();
  assert.equal(Units.toggleUnit(), 'mi');
  assert.equal(Units.toggleUnit(), 'km');
});

// ── Валидация setUnit() ─────────────────────────────────────────────────
test('setUnit() игнорирует некорректное значение без события и записи', () => {
  const { Units, events, calls } = loadUnits();

  const result = Units.setUnit('nautical-miles');

  assert.equal(result, 'km');
  assert.equal(Units.getUnit(), 'km');
  assert.deepEqual(calls.setItem, []);
  assert.equal(events.length, 0);
});

test('setUnit() с уже выбранной единицей не шлёт событие повторно', () => {
  const { Units, events, calls } = loadUnits();

  Units.setUnit('km'); // единица не изменилась

  assert.equal(events.length, 0);
  assert.deepEqual(calls.setItem, []);
});

// ── AC-2: коэффициент перевода и разделитель дробной части ──────────────
test('AC-2: KM_TO_MI равен 0.621371', () => {
  const { Units } = loadUnits();
  assert.equal(Units.KM_TO_MI, 0.621371);
});

test('AC-2/R4: дробная часть отделяется запятой, а не точкой', () => {
  const { Units } = loadUnits();
  assert.ok(Units.formatDistance(12340).includes(','));
  assert.ok(!Units.formatDistance(12340).includes('.'));
});

test('AC-2: точность по умолчанию — 1 знак после запятой', () => {
  const { Units } = loadUnits();
  assert.equal(Units.formatDistance(1000), '1,0 км');
});

// ── Граничные значения formatDistance() ─────────────────────────────────
test('formatDistance() возвращает "-" для отсутствующего/нечислового значения', () => {
  const { Units } = loadUnits();
  assert.equal(Units.formatDistance(null), '-');
  assert.equal(Units.formatDistance(undefined), '-');
  assert.equal(Units.formatDistance(NaN), '-');
});

test('formatDistance() переключает км/м на границе 1000 м', () => {
  const { Units } = loadUnits();
  assert.equal(Units.formatDistance(999), '999 м');
  assert.equal(Units.formatDistance(1000), '1,0 км');
});

// ── Устойчивость к недоступному localStorage (private mode) ─────────────
test('недоступный localStorage не ломает getUnit()/setUnit()', () => {
  const { Units } = loadUnits({ noStorage: true });
  assert.equal(Units.getUnit(), 'km');
  assert.doesNotThrow(() => Units.setUnit('mi'));
  assert.equal(Units.getUnit(), 'mi');
});

// ── Публикация глобального неймспейса window.Units (R7) ─────────────────
test('модуль публикует window.Units для классического подключения', () => {
  const { Units } = loadUnits();
  assert.equal(global.window.Units, Units);
  assert.equal(typeof Units.formatDistance, 'function');
  assert.equal(typeof Units.getUnit, 'function');
  assert.equal(typeof Units.setUnit, 'function');
});
