import { afterEach, beforeEach, expect, test } from 'bun:test';
import { fixture } from './test-fixture';
let f: ReturnType<typeof fixture>;
beforeEach(() => { f = fixture(); }); afterEach(() => f.close());
test('failed revision writes roll back the habit atomically and do not expose database errors', async () => {
  f.sqlite.exec("CREATE TRIGGER reject_revision BEFORE INSERT ON habit_revisions BEGIN SELECT RAISE(ABORT, 'private database detail'); END");
  const response = await f.request('/habits', 'POST', { name: 'Rollback', method: 'manual' });
  expect(response.status).toBe(500); expect(await response.json()).toEqual({ error: 'Internal server error' });
  expect((await f.json('/habits')).habits).toHaveLength(0);
});

test('creation saves color atomically and never puts presentation into requirements', async () => {
  for (const method of ['manual', 'count', 'tasks']) {
    const h = await f.json('/habits', 'POST', { name: method, method, color: '#79618d', ...(method === 'count' ? { target: 8 } : {}) }, 201);
    expect((await f.json(`/habits/${h.id}/calendar-settings`)).mainColor).toBe('#79618d');
    expect((await f.json(`/habits/${h.id}/days/2026-09-04`)).requirements.color).toBeUndefined();
  }
  expect((await f.request('/habits', 'POST', { name: 'Invalid', method: 'manual', color: 'red' })).status).toBe(422);
  expect((await f.json('/habits')).habits).toHaveLength(3);
  f.sqlite.exec("CREATE TRIGGER reject_color BEFORE INSERT ON habit_calendar_settings BEGIN SELECT RAISE(ABORT, 'private color detail'); END");
  const response = await f.request('/habits', 'POST', { name: 'Rollback color', method: 'manual', color: '#426582' });
  expect(response.status).toBe(500);
  expect(await response.json()).toEqual({ error: 'Internal server error' });
  expect((await f.json('/habits')).habits).toHaveLength(3);
});
test('edits save color with requirements atomically, preserving history and other settings', async () => {
  const h = await f.json('/habits', 'POST', { name: 'Water', method: 'count', target: 8, color: '#426582' }, 201);
  await f.json(`/habits/${h.id}/calendar-settings`, 'PUT', { mainColor: '#426582', emptyColor: '#fafafa', shadeCount: 4 });
  f.setTime('2026-09-05T12:00Z');
  await f.json(`/habits/${h.id}`, 'PATCH', { target: 10, color: '#79618d' });
  expect((await f.json(`/habits/${h.id}/days/2026-09-04`)).target).toBe(8);
  expect((await f.json(`/habits/${h.id}/calendar-settings`))).toMatchObject({ mainColor: '#79618d', emptyColor: '#fafafa', shadeCount: 4 });
  expect((await f.json(`/habits/${h.id}`)).color).toBeUndefined();
  expect((await f.request(`/habits/${h.id}`, 'PATCH', { color: 'red' })).status).toBe(422);
  f.sqlite.exec("CREATE TRIGGER reject_color_update BEFORE UPDATE ON habit_calendar_settings BEGIN SELECT RAISE(ABORT, 'private color detail'); END");
  expect((await f.request(`/habits/${h.id}`, 'PATCH', { name: 'Should roll back', target: 12, color: '#58765b' })).status).toBe(500);
  expect((await f.json(`/habits/${h.id}`))).toMatchObject({ name: 'Water', target: 10 });
  expect((await f.json(`/habits/${h.id}/calendar-settings`)).mainColor).toBe('#79618d');
});

test('same-day method switches reset inherited progress even when returning to count', async () => {
  const h = await f.json('/habits', 'POST', { name: 'Carry', method: 'count', target: 8, carryPartialProgress: true }, 201);
  await f.json(`/habits/${h.id}/days/2026-09-04/progress`, 'PUT', { count: 7 }); f.setTime('2026-09-05T12:00Z');
  expect((await f.json(`/habits/${h.id}/days/2026-09-05`)).value).toBe(7);
  await f.json(`/habits/${h.id}`, 'PATCH', { method: 'manual' });
  await f.json(`/habits/${h.id}`, 'PATCH', { method: 'count', target: 8, carryPartialProgress: true });
  expect((await f.json(`/habits/${h.id}/days/2026-09-05`)).value).toBe(0);
});
test('timezone travel across the date line never changes yesterday requirements', async () => {
  const h = await f.json('/habits', 'POST', { name: 'Water', method: 'count', target: 8 }, 201);
  f.setTime('2026-09-05T00:30Z'); await f.json('/today');
  await f.json('/me', 'PATCH', { timezone: 'Pacific/Honolulu' });
  expect((await f.json('/today')).date).toBe('2026-09-05');
  await f.json(`/habits/${h.id}`, 'PATCH', { target: 10 });
  expect((await f.json(`/habits/${h.id}/days/2026-09-04`)).target).toBe(8);
  expect((await f.json(`/habits/${h.id}/days/2026-09-05`)).target).toBe(10);
});
test('a year of multiple habit history materializes and returns every calendar square', async () => {
  const ids: string[] = [];
  for (let i = 0; i < 12; i++) ids.push((await f.json('/habits', 'POST', { name: `Habit ${i}`, method: i % 2 ? 'manual' : 'tasks', ...(i % 2 ? {} : { tasks: [{ name: 'Daily' }] }) }, 201)).id);
  const c = await f.json('/charts', 'POST', { name: 'Year', habitIds: ids }, 201);
  f.setTime('2027-09-04T12:00Z');
  const calendar = await f.json(`/charts/${c.id}/calendar?from=2026-09-04&to=2027-09-04`);
  expect(calendar.days).toHaveLength(366); expect(calendar.days.every((d: any) => d.due === 12 && d.completed === 0)).toBe(true);
  const first = calendar.days[0].habits.find((h: any) => h.method === 'tasks');
  expect(first.tasks[0].expiredAt).toBe(Date.parse('2026-09-04T22:00Z'));
});
