import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { mkdtempSync, rmSync, existsSync, readdirSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runMigrations } from '@main/db/migrations/index.js';
import { BackupService } from '@main/services/BackupService.js';
import { readFileSync, writeFileSync } from 'node:fs';

describe('BackupService.snapshot', () => {
  let dir: string;
  let db: Database.Database;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'inkling-backup-'));
    db = new Database(':memory:');
    runMigrations(db);
    db.prepare(
      `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at)
       VALUES (?, ?, 'pending', ?, ?)`
    ).run('n1', 'hello', '2026-04-26T00:00:00Z', '2026-04-26T00:00:00Z');
  });

  afterEach(() => {
    db.close();
    rmSync(dir, { recursive: true, force: true });
  });

  it('writes inkling-YYYY-MM-DD.sqlite (KST date) to backupDir', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); // 21:00 KST
    const r = await svc.snapshot();
    expect(r.path).toBe(join(dir, 'inkling-2026-04-26.sqlite'));
    expect(existsSync(r.path)).toBe(true);
    expect(r.bytes).toBeGreaterThan(0);
  });

  it('uses KST date even when UTC date differs (around midnight)', async () => {
    // 2026-04-26 23:30 UTC = 2026-04-27 08:30 KST
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T23:30:00Z'));
    const r = await svc.snapshot();
    expect(r.path).toBe(join(dir, 'inkling-2026-04-27.sqlite'));
  });

  it('overwrites same-day backup atomically (no partial files left)', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    await svc.snapshot();
    await svc.snapshot();
    const files = readdirSync(dir).filter((f) => f.startsWith('inkling-'));
    expect(files).toEqual(['inkling-2026-04-26.sqlite']);
    // No leftover .tmp files
    expect(readdirSync(dir).some((f) => f.endsWith('.tmp'))).toBe(false);
  });

  it('snapshot file is a valid SQLite DB containing the source row', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    const r = await svc.snapshot();
    const restored = new Database(r.path, { readonly: true });
    const row = restored.prepare('SELECT id, raw_text FROM notes').get() as
      | { id: string; raw_text: string }
      | undefined;
    expect(row?.id).toBe('n1');
    expect(row?.raw_text).toBe('hello');
    restored.close();
  });

  it('creates backupDir if it does not exist', async () => {
    const fresh = join(dir, 'nested', 'backups');
    expect(existsSync(fresh)).toBe(false);
    const svc = new BackupService(db, fresh, () => new Date('2026-04-26T12:00:00Z'));
    await svc.snapshot();
    expect(existsSync(fresh)).toBe(true);
  });

  it('snapshot file is not zero bytes (regression: empty backup)', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    const r = await svc.snapshot();
    expect(statSync(r.path).size).toBeGreaterThan(100);
  });

  it('cleans up orphan .tmp file when db.backup() fails', async () => {
    // Inject a fake db whose backup() rejects to simulate disk-full / IO-error.
    const fakeDb = {
      backup: async (tmpPath: string) => {
        // Simulate a partial write before the failure (real failures may leave bytes).
        await import('node:fs/promises').then((fs) =>
          fs.writeFile(tmpPath, 'partial-bytes')
        );
        throw new Error('simulated disk full');
      }
    } as unknown as Database.Database;
    const svc = new BackupService(fakeDb, dir, () => new Date('2026-04-26T12:00:00Z'));
    await expect(svc.snapshot()).rejects.toThrow('simulated disk full');
    // No leftover .tmp file
    const remaining = readdirSync(dir);
    expect(remaining.filter((f) => f.endsWith('.tmp'))).toEqual([]);
    // No final file either (write never completed)
    expect(remaining.filter((f) => f.endsWith('.sqlite'))).toEqual([]);
  });
});

describe('BackupService.runDaily', () => {
  let dir: string;
  let db: Database.Database;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'inkling-backup-'));
    db = new Database(':memory:');
    runMigrations(db);
  });

  afterEach(() => {
    db.close();
    rmSync(dir, { recursive: true, force: true });
  });

  it('snapshots when marker is absent', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    const r = await svc.runDaily();
    expect(r.snapshotted).toBe(true);
    expect(existsSync(join(dir, '.last-snapshot'))).toBe(true);
    expect(existsSync(join(dir, 'inkling-2026-04-26.sqlite'))).toBe(true);
  });

  it('skips when marker shows today already snapshotted', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    await svc.runDaily(); // first
    const r = await svc.runDaily(); // second same day
    expect(r.snapshotted).toBe(false);
    expect(r.reason).toMatch(/already/);
  });

  it('snapshots again when marker shows different date', async () => {
    // Pre-seed marker as yesterday
    const dir2 = dir;
    await new BackupService(db, dir2, () => new Date('2026-04-25T12:00:00Z')).runDaily();
    const svc = new BackupService(db, dir2, () => new Date('2026-04-26T12:00:00Z'));
    const r = await svc.runDaily();
    expect(r.snapshotted).toBe(true);
    expect(existsSync(join(dir2, 'inkling-2026-04-26.sqlite'))).toBe(true);
    expect(existsSync(join(dir2, 'inkling-2026-04-25.sqlite'))).toBe(true);
  });

  it('runs rotation after snapshot', async () => {
    // Pre-create an old file that should be rotated out
    const ancient = join(dir, 'inkling-2024-01-01.sqlite');
    writeFileSync(ancient, 'fake');
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    const r = await svc.runDaily();
    expect(r.snapshotted).toBe(true);
    expect(r.removed).toContain('inkling-2024-01-01.sqlite');
    expect(existsSync(ancient)).toBe(false);
  });

  it('marker contains ISO date matching the snapshot file', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    await svc.runDaily();
    const marker = readFileSync(join(dir, '.last-snapshot'), 'utf8').trim();
    expect(marker).toBe('2026-04-26');
  });

  it('lastSnapshotAt returns null when marker absent', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    expect(await svc.lastSnapshotAt()).toBeNull();
  });

  it('lastSnapshotAt returns marker date when present', async () => {
    const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z'));
    await svc.runDaily();
    expect(await svc.lastSnapshotAt()).toBe('2026-04-26');
  });
});
