import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
  createManifest,
  readManifest,
  writeManifest,
  manifestPath,
  heuristicRuntimeAssetDests,
  DEFAULT_SCOPE_LINE,
  MANIFEST_VERSION,
} from './install-manifest.js';

// ─── helpers ─────────────────────────────────────────────────────────────────

let tmpDir: string;

beforeEach(() => {
  tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-manifest-test-'));
});

afterEach(() => {
  rmSync(tmpDir, { recursive: true, force: true });
});

// ─── createManifest ───────────────────────────────────────────────────────────

describe('createManifest', () => {
  it('creates a valid manifest with version 1', () => {
    const m = createManifest('0.0.24', 2);
    expect(m.version).toBe(MANIFEST_VERSION);
    expect(m.cliVersion).toBe('0.0.24');
    expect(m.frameworkVersion).toBe(2);
  });

  it('sets installedAt to an ISO-8601 date string', () => {
    const before = new Date();
    const m = createManifest('0.0.24', 2);
    const after = new Date();
    const ts = new Date(m.installedAt);
    expect(ts.getTime()).toBeGreaterThanOrEqual(before.getTime());
    expect(ts.getTime()).toBeLessThanOrEqual(after.getTime());
  });

  it('starts with empty mutation arrays', () => {
    const m = createManifest('0.0.24', 2);
    expect(m.mutations.directories).toHaveLength(0);
    expect(m.mutations.npmGlobalPackages).toHaveLength(0);
    expect(m.mutations.npmrcLines).toHaveLength(0);
    expect(m.mutations.shellProfileEdits).toHaveLength(0);
    expect(m.mutations.runtimeAssetCopies).toHaveLength(0);
  });

  it('merges partial mutations', () => {
    const m = createManifest('0.0.24', 2, {
      npmGlobalPackages: ['@mosaicstack/mosaic'],
    });
    expect(m.mutations.npmGlobalPackages).toEqual(['@mosaicstack/mosaic']);
    expect(m.mutations.directories).toHaveLength(0);
  });
});

// ─── manifestPath ─────────────────────────────────────────────────────────────

describe('manifestPath', () => {
  it('returns mosaicHome/.install-manifest.json', () => {
    const p = manifestPath('/home/user/.config/mosaic');
    expect(p).toBe('/home/user/.config/mosaic/.install-manifest.json');
  });
});

// ─── writeManifest / readManifest round-trip ─────────────────────────────────

describe('writeManifest + readManifest', () => {
  it('round-trips a manifest through disk', () => {
    const m = createManifest('0.0.24', 2, {
      npmGlobalPackages: ['@mosaicstack/mosaic'],
      npmrcLines: [DEFAULT_SCOPE_LINE],
    });

    writeManifest(tmpDir, m);
    const loaded = readManifest(tmpDir);

    expect(loaded).toBeDefined();
    expect(loaded!.version).toBe(1);
    expect(loaded!.cliVersion).toBe('0.0.24');
    expect(loaded!.mutations.npmGlobalPackages).toEqual(['@mosaicstack/mosaic']);
    expect(loaded!.mutations.npmrcLines).toEqual([DEFAULT_SCOPE_LINE]);
  });

  it('preserves runtimeAssetCopies with backup path', () => {
    const m = createManifest('0.0.24', 2, {
      runtimeAssetCopies: [
        {
          source: '/src/settings.json',
          dest: '/home/user/.claude/settings.json',
          backup: '/home/user/.claude/settings.json.mosaic-bak-20260405120000',
        },
      ],
    });

    writeManifest(tmpDir, m);
    const loaded = readManifest(tmpDir);

    const copies = loaded!.mutations.runtimeAssetCopies;
    expect(copies).toHaveLength(1);
    expect(copies[0]!.backup).toBe('/home/user/.claude/settings.json.mosaic-bak-20260405120000');
  });
});

// ─── readManifest — missing / invalid ────────────────────────────────────────

describe('readManifest error cases', () => {
  it('returns undefined when the file does not exist', () => {
    expect(readManifest('/nonexistent/path')).toBeUndefined();
  });

  it('returns undefined when the file contains invalid JSON', () => {
    const { writeFileSync } = require('node:fs');
    writeFileSync(join(tmpDir, '.install-manifest.json'), 'not json', 'utf8');
    expect(readManifest(tmpDir)).toBeUndefined();
  });

  it('returns undefined when version field is wrong', () => {
    const { writeFileSync } = require('node:fs');
    writeFileSync(
      join(tmpDir, '.install-manifest.json'),
      JSON.stringify({
        version: 99,
        installedAt: new Date().toISOString(),
        cliVersion: '1',
        frameworkVersion: 1,
        mutations: {},
      }),
      'utf8',
    );
    expect(readManifest(tmpDir)).toBeUndefined();
  });
});

// ─── heuristicRuntimeAssetDests ──────────────────────────────────────────────

describe('heuristicRuntimeAssetDests', () => {
  it('returns a non-empty list of absolute paths', () => {
    const dests = heuristicRuntimeAssetDests('/home/user');
    expect(dests.length).toBeGreaterThan(0);
    for (const d of dests) {
      expect(d).toMatch(/^\/home\/user\//);
    }
  });

  it('includes the claude settings.json path', () => {
    const dests = heuristicRuntimeAssetDests('/home/user');
    expect(dests).toContain('/home/user/.claude/settings.json');
  });
});

// ─── DEFAULT_SCOPE_LINE ───────────────────────────────────────────────────────

describe('DEFAULT_SCOPE_LINE', () => {
  it('contains the mosaicstack registry URL', () => {
    expect(DEFAULT_SCOPE_LINE).toContain('mosaicstack');
    expect(DEFAULT_SCOPE_LINE).toContain('@mosaicstack:registry=');
  });
});
