import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
  checkDatabaseSchema,
  SCHEMA_FAIL_REMEDIATION,
  type SchemaCheckDeps,
} from './schema-check.js';

/* ------------------------------------------------------------------ */
/*  Fixture config                                                     */
/* ------------------------------------------------------------------ */

const tmpDirs: string[] = [];

function writeConfig(cfg: Record<string, unknown>): string {
  const dir = mkdtempSync(join(tmpdir(), 'schema-check-'));
  tmpDirs.push(dir);
  const path = join(dir, 'mosaic.config.json');
  writeFileSync(path, JSON.stringify(cfg));
  return path;
}

const STANDALONE_CFG = {
  tier: 'standalone',
  storage: { type: 'postgres', url: 'postgresql://u:p@localhost:5434/x' },
  queue: { type: 'bullmq', url: 'redis://localhost:6380' },
  memory: { type: 'keyword' },
};

const LOCAL_CFG = {
  tier: 'local',
  storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' },
  queue: { type: 'local', dataDir: '.mosaic/queue' },
  memory: { type: 'keyword' },
};

function deps(overrides: Partial<SchemaCheckDeps> = {}): SchemaCheckDeps {
  return {
    runMigrations: vi.fn().mockResolvedValue(undefined),
    getMigrationStatus: vi.fn().mockResolvedValue({
      appliedCount: 17,
      expectedCount: 17,
      expectedLastTag: '0016_x',
      complete: true,
    }),
    ...overrides,
  };
}

/* ------------------------------------------------------------------ */
/*  Tests                                                              */
/* ------------------------------------------------------------------ */

describe('checkDatabaseSchema', () => {
  beforeEach(() => {
    process.env['MOSAIC_CONFIG'] = '';
    vi.stubEnv('DATABASE_URL', 'postgresql://env:env@localhost:9999/env');
  });

  afterEach(() => {
    vi.unstubAllEnvs();
    for (const d of tmpDirs.splice(0)) rmSync(d, { recursive: true, force: true });
  });

  it('passes when the ledger matches the shipped journal', async () => {
    const cfg = writeConfig(STANDALONE_CFG);
    const d = deps();
    const result = await checkDatabaseSchema(d, cfg);
    expect(result.status).toBe('ok');
    expect(result.detail).toContain('17/17');
    expect(d.runMigrations).toHaveBeenCalledWith(STANDALONE_CFG.storage.url);
    expect(d.getMigrationStatus).toHaveBeenCalledWith(STANDALONE_CFG.storage.url);
  });

  it('FAILS when the ledger is incomplete — the #1389 empty-database signature', async () => {
    const cfg = writeConfig(STANDALONE_CFG);
    const d = deps({
      getMigrationStatus: vi.fn().mockResolvedValue({
        appliedCount: 0,
        expectedCount: 17,
        expectedLastTag: '0016_x',
        complete: false,
      }),
    });
    const result = await checkDatabaseSchema(d, cfg);
    expect(result.status).toBe('fail');
    if (result.status !== 'fail') throw new Error('expected fail');
    expect(result.detail).toContain('0/17');
    expect(result.remediation).toBe(SCHEMA_FAIL_REMEDIATION);
    expect(result.remediation).toContain('#1389');
  });

  it('FAILS when the ledger is only PARTIALLY migrated (#1402 upgrade case)', async () => {
    const cfg = writeConfig(STANDALONE_CFG);
    const d = deps({
      getMigrationStatus: vi.fn().mockResolvedValue({
        appliedCount: 9,
        expectedCount: 17,
        expectedLastTag: '0016_x',
        complete: false,
      }),
    });
    const result = await checkDatabaseSchema(d, cfg);
    expect(result.status).toBe('fail');
    expect(result.detail).toContain('9/17');
  });

  it('FAILS (never crashes) when the migration run itself throws', async () => {
    const cfg = writeConfig(STANDALONE_CFG);
    const d = deps({
      runMigrations: vi.fn().mockRejectedValue(new Error('connection refused')),
    });
    const result = await checkDatabaseSchema(d, cfg);
    expect(result.status).toBe('fail');
    expect(result.detail).toContain('connection refused');
  });

  it('skips the local tier (gateway migrates its own PGlite at startup)', async () => {
    const cfg = writeConfig(LOCAL_CFG);
    const d = deps();
    const result = await checkDatabaseSchema(d, cfg);
    expect(result.status).toBe('skipped');
    expect(d.runMigrations).not.toHaveBeenCalled();
  });
});

/* ------------------------------------------------------------------ */
/*  Config resolution priority                                         */
/* ------------------------------------------------------------------ */

describe('resolveSchemaCheckConfigPath', () => {
  it('prefers the daemon-written gateway config over cwd copies', async () => {
    const { resolveSchemaCheckConfigPath } = await import('./schema-check.js');
    const cwdCfg = writeConfig(STANDALONE_CFG); // in a temp dir
    const daemonDir = mkdtempSync(join(tmpdir(), 'schema-check-daemon-'));
    tmpDirs.push(daemonDir);
    const daemonHome = join(daemonDir, '.config', 'mosaic', 'gateway');
    mkdirSync(daemonHome, { recursive: true });
    writeFileSync(join(daemonHome, 'mosaic.config.json'), JSON.stringify(LOCAL_CFG));

    const prevHome = process.env['HOME'];
    vi.stubEnv('HOME', daemonDir);
    vi.stubEnv('MOSAIC_CONFIG', '');
    try {
      const resolved = resolveSchemaCheckConfigPath();
      // Must NOT pick the cwd copy (cwd is the vitest project dir, not our
      // temp dir — so the only resolvable candidates are daemon + $HOME/.mosaic).
      expect(resolved).toBe(join(daemonHome, 'mosaic.config.json'));
      void cwdCfg;
    } finally {
      if (prevHome !== undefined) vi.stubEnv('HOME', prevHome);
    }
  });

  it('gives MOSAIC_CONFIG NO authority (N1, review 285): env never overrides file resolution', async () => {
    const { resolveSchemaCheckConfigPath } = await import('./schema-check.js');
    const daemonDir = mkdtempSync(join(tmpdir(), 'schema-check-env-'));
    tmpDirs.push(daemonDir);
    const daemonHome = join(daemonDir, '.config', 'mosaic', 'gateway');
    mkdirSync(daemonHome, { recursive: true });
    writeFileSync(join(daemonHome, 'mosaic.config.json'), JSON.stringify(LOCAL_CFG));
    // A stale env var pointing at a DIFFERENT file must be ignored entirely:
    const decoyDir = mkdtempSync(join(tmpdir(), 'schema-check-decoy-'));
    tmpDirs.push(decoyDir);
    const decoyPath = join(decoyDir, 'mosaic.config.json');
    writeFileSync(decoyPath, JSON.stringify(STANDALONE_CFG));

    const prevHome = process.env['HOME'];
    vi.stubEnv('HOME', daemonDir);
    vi.stubEnv('MOSAIC_CONFIG', decoyPath);
    try {
      const resolved = resolveSchemaCheckConfigPath();
      expect(resolved).toBe(join(daemonHome, 'mosaic.config.json'));
      expect(resolved).not.toBe(decoyPath);
    } finally {
      if (prevHome !== undefined) vi.stubEnv('HOME', prevHome);
    }
  });
});
