import { describe, test, expect, vi, afterEach, beforeAll, afterAll } from 'vitest';
import { createServer } from 'node:http';
import { createLab } from '../public/model.js';
const lab = createLab(process.env.LAB_VARIANT || 'fixed');
let server, base;
let cleanups=[];
const tracked=fn=>{cleanups.push(fn);return fn;};
beforeAll(async () => {
  server = createServer((req,res) => {
    res.setHeader('Content-Type','application/json');
    if(req.url === '/customer') { res.end(JSON.stringify({id:7,name:'Анна'})); }
    else if(req.url === '/invalid') { res.end(JSON.stringify({name:7})); }
    else { res.statusCode=503; res.end(JSON.stringify({error:'Service unavailable'})); }
  });
  await new Promise(resolve => server.listen(0,'127.0.0.1',resolve));
  base='http://127.0.0.1:'+server.address().port;
});
afterAll(async () => { server.closeAllConnections(); await new Promise(resolve=>server.close(resolve)); });
afterEach(() => { for(const cleanup of cleanups) cleanup();cleanups=[];vi.clearAllTimers();vi.useRealTimers();vi.restoreAllMocks();document.body.replaceChildren(); });

describe('01 · Цена и границы', () => {
  test('3 × 19900 копеек со скидкой 10% = 53730',()=>expect(lab.total(19900,3,10)).toBe(53730));
  test('нулевая цена остаётся нулём',()=>expect(lab.total(0,1)).toBe(0));
  test('дробное количество отклоняется',()=>expect(()=>lab.total(100,1.5)).toThrow(RangeError));
  test('скидка выше 100% отклоняется',()=>expect(()=>lab.total(100,1,101)).toThrow(RangeError));
  test('небезопасное целое и переполнение суммы отклоняются',()=>{expect(()=>lab.total(1e308,100)).toThrow(RangeError);expect(()=>lab.total(Number.MAX_SAFE_INTEGER,2)).toThrow(RangeError);});
});
describe('02 · Таблица случаев', () => {
  test.each([
    ['anna@example.com',true],['  anna@example.com  ',true],['',false],['anna@',false],['a b@example.com',false],['example.com',false]
  ])('email %j → %s',(value,expected)=>expect(lab.validateEmail(value)).toBe(expected));
});
describe('03 · Async и реальный HTTP', () => {
  test('await получает JSON настоящего локального сервера',async()=>await expect(lab.loadCustomer(fetch,base+'/customer')).resolves.toEqual({id:7,name:'Анна'}));
  test('HTTP 503 отклоняет Promise',async()=>await expect(lab.loadCustomer(fetch,base+'/unavailable')).rejects.toThrow('HTTP 503'));
  test('неверная форма данных отклоняется',async()=>await expect(lab.loadCustomer(fetch,base+'/invalid')).rejects.toThrow('Некорректный клиент'));
});
describe('04 · Mock границы API', () => {
  test('передаёт копейки и очищенный email',async()=>{
    const post=vi.fn().mockResolvedValue({id:42});
    await expect(lab.saveOrder({post},'  anna@example.com  ',53730)).resolves.toEqual({id:42});
    expect(post).toHaveBeenCalledExactlyOnceWith('/orders',{email:'anna@example.com',totalCents:53730});
  });
  test('не отправляет неверный контакт',async()=>{const post=vi.fn();await expect(lab.saveOrder({post},'bad',10)).rejects.toThrow();expect(post).not.toHaveBeenCalled();});
});
describe('05 · Debounce и fake timers', () => {
  test('оставляет только последний поисковый запрос',()=>{
    vi.useFakeTimers(); const callback=vi.fn(), search=lab.debounceSearch(callback,300);
    search('а');search('ан');search('анна');vi.advanceTimersByTime(299);expect(callback).not.toHaveBeenCalled();
    vi.advanceTimersByTime(1);expect(callback).toHaveBeenCalledExactlyOnceWith('анна');search.cancel();
  });
  test('cancel очищает отложенную работу',()=>{
    vi.useFakeTimers();const callback=vi.fn(),search=lab.debounceSearch(callback);search('x');search.cancel();vi.runAllTimers();expect(callback).not.toHaveBeenCalled();
  });
});
describe('06 · DOM-форма в JSDOM',()=>{
  test('не сохраняет пустой email и фокусирует поле',()=>{
    const cleanup=tracked(lab.mountContactForm(document.body));document.querySelector('form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));
    expect(document.querySelector('[role=status]').textContent).toBe('Укажите корректный email');
    expect(document.activeElement).toBe(document.querySelector('input'));expect(document.querySelector('input').getAttribute('aria-invalid')).toBe('true');cleanup();
  });
  test('сохраняет валидный контакт',()=>{const cleanup=tracked(lab.mountContactForm(document.body));document.querySelector('input').value='anna@example.com';document.querySelector('form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));expect(document.querySelector('[role=status]').textContent).toBe('Сохранено: anna@example.com');cleanup();});
});
describe('07 · Disclosure: DOM preflight, не Browser Mode',()=>{
  test('синхронизирует aria-expanded с панелью',()=>{
    const cleanup=tracked(lab.mountDisclosure(document.body)),toggle=document.querySelector('#details-toggle');toggle.click();
    expect(toggle.getAttribute('aria-expanded')).toBe('true');expect(document.querySelector('#details-panel').hidden).toBe(false);cleanup();
  });
  test('Escape закрывает и возвращает фокус',()=>{
    const cleanup=tracked(lab.mountDisclosure(document.body)),toggle=document.querySelector('#details-toggle');toggle.click();document.querySelector('input').dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));
    expect(document.querySelector('#details-panel').hidden).toBe(true);expect(document.activeElement).toBe(toggle);cleanup();
  });
});
describe('08 · Snapshot и coverage',()=>{
  test('стабильное представление заказа',()=>{
    expect(lab.summary({id:42,customer:'Анна',cents:53730,paid:false})).toMatchInlineSnapshot(`
      {
        "amount": "537.30",
        "currency": "RUB",
        "customer": "Анна",
        "id": 42,
        "state": "pending",
      }
    `);
  });
  test('самовывоз бесплатен; другие ветки намеренно не проверены здесь',()=>expect(lab.deliveryFee('pickup')).toBe(0));
});
describe('09 · CI: все проверки обязательны',()=>{
  test('падение одной проверки запрещает выпуск',()=>expect(lab.canPublish([true,false,true])).toBe(false));
  test('полностью зелёный набор разрешает следующий шаг',()=>expect(lab.canPublish([true,true])).toBe(true));
  test('пустой набор не является доказательством',()=>expect(lab.canPublish([])).toBe(false));
});
