import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { mkdtempSync, rmSync, mkdirSync, existsSync, closeSync, openSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { runMigrations } from '@main/db/migrations/index.js';
import { NoteRepository } from '@main/repository/NoteRepository.js';
import { MediaStore } from '@main/services/MediaStore.js';
import { ExportService } from '@main/services/ExportService.js';
import { ImportService } from '@main/services/ImportService.js';
import { SyncService } from '@main/services/SyncService.js';

const execFileAsync = promisify(execFile);

let emptyConfigPath: string;
let emptyConfigDir: string;

function gitEnv(): NodeJS.ProcessEnv {
  return {
    ...process.env,
    GIT_CONFIG_GLOBAL: emptyConfigPath,
    GIT_CONFIG_SYSTEM: emptyConfigPath,
    GIT_AUTHOR_NAME: 'Test',
    GIT_AUTHOR_EMAIL: 'test@inkling.local',
    GIT_COMMITTER_NAME: 'Test',
    GIT_COMMITTER_EMAIL: 'test@inkling.local'
  };
}

async function initRepoWithBareRemote(workDir: string): Promise<string> {
  mkdirSync(workDir, { recursive: true });
  const env = gitEnv();
  await execFileAsync('git', ['init', '-b', 'main', workDir], { env });
  await execFileAsync('git', ['-C', workDir, 'config', 'user.email', 'test@inkling.local'], { env });
  await execFileAsync('git', ['-C', workDir, 'config', 'user.name', 'Test'], { env });
  // Bare local remote so push works without network.
  const remoteDir = `${workDir}-remote.git`;
  await execFileAsync('git', ['init', '--bare', '-b', 'main', remoteDir], { env });
  await execFileAsync('git', ['-C', workDir, 'remote', 'add', 'origin', remoteDir], { env });
  return remoteDir;
}

describe('SyncService', () => {
  let profileDir: string;
  let db: Database.Database;
  let repo: NoteRepository;
  let mediaStore: MediaStore;
  let exportSvc: ExportService;
  let importSvc: ImportService;
  let svc: SyncService;
  let remoteDir: string | null = null;
  let prevEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    emptyConfigDir = mkdtempSync(join(tmpdir(), 'inkling-sync-cfg-'));
    emptyConfigPath = join(emptyConfigDir, 'empty.gitconfig');
    closeSync(openSync(emptyConfigPath, 'w'));

    prevEnv = { ...process.env };
    Object.assign(process.env, {
      GIT_CONFIG_GLOBAL: emptyConfigPath,
      GIT_CONFIG_SYSTEM: emptyConfigPath,
      GIT_AUTHOR_NAME: 'Test',
      GIT_AUTHOR_EMAIL: 'test@inkling.local',
      GIT_COMMITTER_NAME: 'Test',
      GIT_COMMITTER_EMAIL: 'test@inkling.local'
    });

    profileDir = mkdtempSync(join(tmpdir(), 'inkling-sync-'));
    mkdirSync(join(profileDir, 'media'), { recursive: true });
    db = new Database(':memory:');
    runMigrations(db);
    repo = new NoteRepository(db);
    mediaStore = new MediaStore(profileDir);
    exportSvc = new ExportService(repo, mediaStore, () => new Date('2026-04-26T12:00:00Z'));
    importSvc = new ImportService(repo, mediaStore);
    svc = new SyncService(profileDir, exportSvc, importSvc, () => new Date('2026-04-26T12:00:00Z'));
  });

  afterEach(() => {
    db.close();
    rmSync(profileDir, { recursive: true, force: true });
    if (remoteDir !== null) {
      rmSync(remoteDir, { recursive: true, force: true });
      remoteDir = null;
    }
    rmSync(emptyConfigDir, { recursive: true, force: true });
    for (const k of Object.keys(process.env)) delete process.env[k];
    Object.assign(process.env, prevEnv);
  });

  it('isConfigured returns false before sync dir is set up', async () => {
    expect(await svc.isConfigured()).toBe(false);
  });

  it('sync returns not_configured when sync dir missing', async () => {
    const r = await svc.sync();
    expect(r.ok).toBe(false);
    expect(r.reason).toBe('not_configured');
  });

  it('isConfigured returns true after init + remote', async () => {
    remoteDir = await initRepoWithBareRemote(svc.getSyncDir());
    expect(await svc.isConfigured()).toBe(true);
  });

  it('sync writes export tree, commits, pushes — first run', async () => {
    remoteDir = await initRepoWithBareRemote(svc.getSyncDir());
    repo.create({ rawText: 'first note' });
    const r = await svc.sync();
    expect(r.ok).toBe(true);
    expect(r.changed).toBe(true);
    expect(r.pushed).toBe(true);
    expect(r.localSha).toMatch(/^[0-9a-f]{40}$/);
    expect(existsSync(join(svc.getSyncDir(), 'manifest.json'))).toBe(true);
    expect(existsSync(join(svc.getSyncDir(), 'notes'))).toBe(true);
    expect(existsSync(join(svc.getSyncDir(), 'index.jsonl'))).toBe(true);
  });

  it('sync returns changed=false when DB unchanged on second run', async () => {
    remoteDir = await initRepoWithBareRemote(svc.getSyncDir());
    repo.create({ rawText: 'first note' });
    const first = await svc.sync();
    expect(first.ok).toBe(true);
    expect(first.changed).toBe(true);
    // Re-sync without DB change. With fixed now() → identical files → git sees no local change.
    // New bidirectional flow: always does fetch+rebase+re-import+push.
    const second = await svc.sync();
    expect(second.ok).toBe(true);
    expect(second.changed).toBe(false); // no local commit + importedCount=0
    expect(second.pushed).toBe(true);   // push always runs on success
  });
});
