import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
  loadState,
  saveState,
  emptyState,
  sha256Of,
  hasFileBeenModified,
  detectModifiedFiles,
  type FileState,
} from '../../src/utils/skills-state.js';

describe('skills-state', () => {
  let tmp: string;

  beforeEach(async () => {
    tmp = await mkdtemp(join(tmpdir(), 'mcpctl-skills-state-'));
  });

  afterEach(async () => {
    await rm(tmp, { recursive: true, force: true });
  });

  describe('sha256Of', () => {
    it('is deterministic and prefixed', () => {
      expect(sha256Of('hello')).toMatch(/^sha256:[0-9a-f]{64}$/);
      expect(sha256Of('hello')).toBe(sha256Of('hello'));
      expect(sha256Of('hello')).not.toBe(sha256Of('hellp'));
    });
  });

  describe('load / save', () => {
    it('returns empty state when file does not exist', async () => {
      const state = await loadState(join(tmp, 'no-such.json'));
      expect(state.skills).toEqual({});
      expect(state.lastSync).toBeNull();
    });

    it('round-trips state', async () => {
      const path = join(tmp, 'state.json');
      const state = emptyState();
      state.lastSync = '2026-05-07T00:00:00.000Z';
      state.lastSyncProject = 'demo';
      state.skills['my-skill'] = {
        id: 'cuid-x',
        semver: '0.1.0',
        contentHash: sha256Of('body'),
        scope: 'global',
        installDir: '/tmp/foo',
        files: { 'SKILL.md': { sha256: sha256Of('hi'), size: 2 } },
        postInstallHash: null,
        lastSyncedAt: '2026-05-07T00:00:00.000Z',
      };
      await saveState(state, path);
      const loaded = await loadState(path);
      expect(loaded).toEqual(state);
    });

    it('starts fresh on schema-version drift', async () => {
      const path = join(tmp, 'state.json');
      await writeFile(path, JSON.stringify({ schemaVersion: 99, skills: { x: {} } }));
      const state = await loadState(path);
      expect(state.schemaVersion).toBe(1);
      expect(state.skills).toEqual({});
    });
  });

  describe('hasFileBeenModified', () => {
    it('false when content matches recorded hash + size', async () => {
      const dir = join(tmp, 'sk');
      await mkdir(dir);
      await writeFile(join(dir, 'SKILL.md'), 'hello');
      const recorded: FileState = { sha256: sha256Of('hello'), size: 5 };
      expect(await hasFileBeenModified(dir, 'SKILL.md', recorded)).toBe(false);
    });

    it('true when content differs', async () => {
      const dir = join(tmp, 'sk');
      await mkdir(dir);
      await writeFile(join(dir, 'SKILL.md'), 'edited');
      const recorded: FileState = { sha256: sha256Of('hello'), size: 5 };
      expect(await hasFileBeenModified(dir, 'SKILL.md', recorded)).toBe(true);
    });

    it('true when file is missing', async () => {
      const recorded: FileState = { sha256: sha256Of('hello'), size: 5 };
      expect(await hasFileBeenModified(tmp, 'missing.md', recorded)).toBe(true);
    });
  });

  describe('detectModifiedFiles', () => {
    it('returns the list of edited paths', async () => {
      const dir = join(tmp, 'sk');
      await mkdir(dir);
      await writeFile(join(dir, 'SKILL.md'), 'pristine');
      await writeFile(join(dir, 'extra.md'), 'edited');
      const result = await detectModifiedFiles(dir, {
        'SKILL.md': { sha256: sha256Of('pristine'), size: 8 },
        'extra.md': { sha256: sha256Of('original'), size: 8 },
      });
      expect(result).toEqual(['extra.md']);
    });
  });
});
