/**
 * tripSyncManager unit tests.
 *
 * Covers: trip filtering (shouldCache/isStale), bundle fetch → Dexie upsert,
 * stale trip eviction, offline guard, file blob caching.
 */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import 'fake-indexeddb/auto';
import { server } from '../../helpers/msw/server';
import { http, HttpResponse } from 'msw';
import { tripSyncManager } from '../../../src/sync/tripSyncManager';
import { offlineDb, clearAll, upsertTrip } from '../../../src/db/offlineDb';
import {
  buildTrip,
  buildDay,
  buildPlace,
  buildPackingItem,
  buildTodoItem,
  buildBudgetItem,
  buildReservation,
  buildTripFile,
} from '../../helpers/factories';

// Helper to get today ± N days as YYYY-MM-DD
function dateOffset(days: number): string {
  const d = new Date();
  d.setDate(d.getDate() + days);
  return d.toISOString().slice(0, 10);
}

function makeBundle(tripId: number) {
  const trip = buildTrip({ id: tripId, end_date: dateOffset(3) });
  return {
    trip,
    days: [buildDay({ trip_id: tripId, assignments: [], notes_items: [] })],
    places: [buildPlace({ trip_id: tripId })],
    packingItems: [buildPackingItem({ trip_id: tripId })],
    todoItems: [buildTodoItem({ trip_id: tripId })],
    budgetItems: [buildBudgetItem({ trip_id: tripId })],
    reservations: [buildReservation({ trip_id: tripId })],
    files: [buildTripFile({ trip_id: tripId, url: `/api/trips/${tripId}/files/99/download`, mime_type: 'application/pdf' })],
  };
}

beforeEach(async () => {
  await clearAll();
  tripSyncManager._resetSyncing();
  Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true });
  // Stub fetch for blob caching (used by cacheFilesForTrip)
  vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
    ok: true,
    blob: async () => new Blob(['data'], { type: 'application/pdf' }),
  }));
});

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
});

// ── offline guard ─────────────────────────────────────────────────────────────

describe('tripSyncManager.syncAll — offline guard', () => {
  it('does nothing when offline', async () => {
    Object.defineProperty(navigator, 'onLine', { value: false });

    let listed = false;
    server.use(
      http.get('/api/trips', () => { listed = true; return HttpResponse.json({ trips: [] }); }),
    );

    await tripSyncManager.syncAll();
    expect(listed).toBe(false);
  });
});

// ── trip filtering ─────────────────────────────────────────────────────────────

describe('tripSyncManager.syncAll — trip filtering', () => {
  it('caches ongoing trips (end_date >= today)', async () => {
    const tripId = 100;
    const bundle = makeBundle(tripId);

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(2) })] }),
      ),
      http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)),
    );

    await tripSyncManager.syncAll();

    const cached = await offlineDb.trips.get(tripId);
    expect(cached).toBeDefined();
    expect(cached!.id).toBe(tripId);
  });

  it('caches trips with no end_date', async () => {
    const tripId = 101;
    const bundle = makeBundle(tripId);
    const trip = buildTrip({ id: tripId, end_date: null as unknown as string });

    server.use(
      http.get('/api/trips', () => HttpResponse.json({ trips: [trip] })),
      http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json({ ...bundle, trip })),
    );

    await tripSyncManager.syncAll();
    expect(await offlineDb.trips.get(tripId)).toBeDefined();
  });

  it('does not cache past trips (end_date < today)', async () => {
    const tripId = 102;

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(-1) })] }),
      ),
    );

    // Bundle should NOT be called for past trips
    let bundleCalled = false;
    server.use(
      http.get(`/api/trips/${tripId}/bundle`, () => {
        bundleCalled = true;
        return HttpResponse.json({});
      }),
    );

    await tripSyncManager.syncAll();
    expect(bundleCalled).toBe(false);
    expect(await offlineDb.trips.get(tripId)).toBeUndefined();
  });
});

// ── stale eviction ─────────────────────────────────────────────────────────────

describe('tripSyncManager.syncAll — stale eviction', () => {
  it('evicts trips that ended more than 7 days ago', async () => {
    const staleId = 200;
    // Seed Dexie as if previously cached
    await upsertTrip(buildTrip({ id: staleId, end_date: dateOffset(-8) }));

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: staleId, end_date: dateOffset(-8) })] }),
      ),
    );

    await tripSyncManager.syncAll();
    expect(await offlineDb.trips.get(staleId)).toBeUndefined();
  });

  it('does NOT evict trips that ended exactly 6 days ago', async () => {
    const recentId = 201;
    const bundle = makeBundle(recentId);
    const trip = buildTrip({ id: recentId, end_date: dateOffset(-6) });

    server.use(
      http.get('/api/trips', () => HttpResponse.json({ trips: [trip] })),
      http.get(`/api/trips/${recentId}/bundle`, () => HttpResponse.json({ ...bundle, trip })),
    );

    await tripSyncManager.syncAll();
    // end_date = -6 days: still within 7d window, but < today so not cached
    // i.e., shouldCache is false (end_date < today) so won't be fetched
    // but also isStale is false (end_date = -6 >= cutoff -7), so won't be evicted
    // → trip should simply not appear in Dexie (not cached, not evicted pre-seeded data)
    expect(await offlineDb.trips.get(recentId)).toBeUndefined();
  });
});

// ── bundle upsert ──────────────────────────────────────────────────────────────

describe('tripSyncManager.syncAll — bundle upsert', () => {
  it('writes all bundle entities to Dexie', async () => {
    const tripId = 300;
    const bundle = makeBundle(tripId);

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }),
      ),
      http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)),
    );

    await tripSyncManager.syncAll();

    expect(await offlineDb.trips.get(tripId)).toBeDefined();
    expect(await offlineDb.days.where('trip_id').equals(tripId).count()).toBe(1);
    expect(await offlineDb.places.where('trip_id').equals(tripId).count()).toBe(1);
    expect(await offlineDb.packingItems.where('trip_id').equals(tripId).count()).toBe(1);
    expect(await offlineDb.todoItems.where('trip_id').equals(tripId).count()).toBe(1);
    expect(await offlineDb.budgetItems.where('trip_id').equals(tripId).count()).toBe(1);
    expect(await offlineDb.reservations.where('trip_id').equals(tripId).count()).toBe(1);
    expect(await offlineDb.tripFiles.where('trip_id').equals(tripId).count()).toBe(1);
  });

  it('writes syncMeta with lastSyncedAt', async () => {
    const tripId = 301;
    const bundle = makeBundle(tripId);

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }),
      ),
      http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)),
    );

    const before = Date.now();
    await tripSyncManager.syncAll();
    const after = Date.now();

    const meta = await offlineDb.syncMeta.get(tripId);
    expect(meta).toBeDefined();
    expect(meta!.lastSyncedAt).toBeGreaterThanOrEqual(before);
    expect(meta!.lastSyncedAt).toBeLessThanOrEqual(after);
  });
});

// ── file blob caching ──────────────────────────────────────────────────────────

describe('tripSyncManager — file blob caching', () => {
  it('caches non-photo files after bundle sync', async () => {
    const tripId = 400;
    const bundle = makeBundle(tripId);

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }),
      ),
      http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)),
    );

    await tripSyncManager.syncAll();

    // Give fire-and-forget a tick
    await new Promise(r => setTimeout(r, 50));

    const cached = await offlineDb.blobCache.toArray();
    expect(cached.length).toBeGreaterThan(0);
    expect(cached[0].url).toContain('/download');
  });

  it('does not cache photo files (image/* MIME)', async () => {
    const tripId = 401;
    const photoFile = buildTripFile({
      trip_id: tripId,
      mime_type: 'image/jpeg',
      url: `/api/trips/${tripId}/files/77/download`,
    });
    const bundle = {
      ...makeBundle(tripId),
      files: [photoFile],
    };

    server.use(
      http.get('/api/trips', () =>
        HttpResponse.json({ trips: [buildTrip({ id: tripId, end_date: dateOffset(5) })] }),
      ),
      http.get(`/api/trips/${tripId}/bundle`, () => HttpResponse.json(bundle)),
    );

    await tripSyncManager.syncAll();
    await new Promise(r => setTimeout(r, 50));

    const cached = await offlineDb.blobCache.toArray();
    expect(cached.length).toBe(0);
  });
});
