import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
  buildPrelaunchMaintenanceCacheKey,
  runCachedPrelaunchMaintenanceTask,
} from '@electron/gateway/prelaunch-maintenance-cache';

describe('prelaunch maintenance cache', () => {
  let tempDir: string;
  let cachePath: string;

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'clawx-prelaunch-cache-'));
    cachePath = join(tempDir, 'cache.json');
  });

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

  it('runs a task on cache miss and skips it when the cache key is unchanged', () => {
    const task = vi.fn();
    const cacheKey = buildPrelaunchMaintenanceCacheKey({
      task: 'skills-symlink-cleanup',
      appVersion: '1.0.0',
      rootSignature: 'mtime-a',
    });

    expect(runCachedPrelaunchMaintenanceTask(
      'skills-symlink-cleanup',
      cacheKey,
      task,
      { cachePath },
    )).toEqual({ executed: true, reason: 'cache-miss' });
    expect(task).toHaveBeenCalledTimes(1);

    expect(runCachedPrelaunchMaintenanceTask(
      'skills-symlink-cleanup',
      cacheKey,
      task,
      { cachePath },
    )).toEqual({ executed: false, reason: 'cache-hit' });
    expect(task).toHaveBeenCalledTimes(1);
  });

  it('stores the post-task cache key when the task mutates signed inputs', () => {
    let rootSignature = 'dirty';
    const cacheKey = () => buildPrelaunchMaintenanceCacheKey({
      task: 'skills-symlink-cleanup',
      appVersion: '1.0.0',
      rootSignature,
    });
    const task = vi.fn(() => {
      rootSignature = 'clean';
    });

    expect(runCachedPrelaunchMaintenanceTask(
      'skills-symlink-cleanup',
      cacheKey,
      task,
      { cachePath },
    )).toEqual({ executed: true, reason: 'cache-miss' });
    expect(task).toHaveBeenCalledTimes(1);

    const writtenCache = JSON.parse(readFileSync(cachePath, 'utf-8'));
    expect(writtenCache.tasks['skills-symlink-cleanup'].key).toBe(cacheKey());

    expect(runCachedPrelaunchMaintenanceTask(
      'skills-symlink-cleanup',
      cacheKey,
      task,
      { cachePath },
    )).toEqual({ executed: false, reason: 'cache-hit' });
    expect(task).toHaveBeenCalledTimes(1);
  });

  it('does not cache a task that reports maintenance failure', () => {
    const cacheKey = buildPrelaunchMaintenanceCacheKey({
      task: 'plugin-maintenance',
      appVersion: '1.0.0',
      configuredChannels: ['feishu'],
    });
    const task = vi.fn()
      .mockReturnValueOnce(false)
      .mockReturnValueOnce(true);

    expect(runCachedPrelaunchMaintenanceTask(
      'plugin-maintenance',
      cacheKey,
      task,
      { cachePath },
    )).toEqual({ executed: true, reason: 'task-failed' });
    expect(task).toHaveBeenCalledTimes(1);

    expect(runCachedPrelaunchMaintenanceTask(
      'plugin-maintenance',
      cacheKey,
      task,
      { cachePath },
    )).toEqual({ executed: true, reason: 'cache-miss' });
    expect(task).toHaveBeenCalledTimes(2);
  });

  it('reruns a task when the cache key changes', () => {
    const task = vi.fn();
    const firstKey = buildPrelaunchMaintenanceCacheKey({
      task: 'runtime-deps-cleanup',
      openclawDir: '/old/openclaw',
    });
    const secondKey = buildPrelaunchMaintenanceCacheKey({
      task: 'runtime-deps-cleanup',
      openclawDir: '/new/openclaw',
    });

    runCachedPrelaunchMaintenanceTask('runtime-deps-cleanup', firstKey, task, { cachePath });
    const result = runCachedPrelaunchMaintenanceTask('runtime-deps-cleanup', secondKey, task, { cachePath });

    expect(result).toEqual({ executed: true, reason: 'cache-miss' });
    expect(task).toHaveBeenCalledTimes(2);
  });

  it('treats cache schema changes as misses', () => {
    const task = vi.fn();
    const cacheKey = buildPrelaunchMaintenanceCacheKey({
      task: 'plugin-maintenance',
      configuredChannels: ['feishu'],
    });
    writeFileSync(cachePath, JSON.stringify({
      schemaVersion: 0,
      tasks: {
        'plugin-maintenance': {
          key: cacheKey,
          updatedAt: new Date().toISOString(),
        },
      },
    }), 'utf-8');

    const result = runCachedPrelaunchMaintenanceTask('plugin-maintenance', cacheKey, task, { cachePath });

    expect(result).toEqual({ executed: true, reason: 'cache-miss' });
    expect(task).toHaveBeenCalledTimes(1);
    expect(JSON.parse(readFileSync(cachePath, 'utf-8')).schemaVersion).toBe(1);
  });

  it('runs conservatively when the cache file cannot be read', () => {
    const task = vi.fn();
    const blockedCachePath = join(tempDir, 'blocked-cache');
    mkdirSync(blockedCachePath);
    const cacheKey = buildPrelaunchMaintenanceCacheKey({
      task: 'plugin-maintenance',
      configuredChannels: [],
    });

    const result = runCachedPrelaunchMaintenanceTask(
      'plugin-maintenance',
      cacheKey,
      task,
      { cachePath: blockedCachePath },
    );

    expect(result).toEqual({ executed: true, reason: 'cache-unavailable' });
    expect(task).toHaveBeenCalledTimes(1);
  });
});