/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as fs from 'node:fs';
import { TrustedHooksManager } from './trustedHooks.js';
import { Storage } from '../config/storage.js';
import { HookEventName, HookType, type HookDefinition } from './types.js';

vi.mock('node:fs');
vi.mock('../config/storage.js');
vi.mock('../utils/debugLogger.js', () => ({
  debugLogger: {
    warn: vi.fn(),
    error: vi.fn(),
    log: vi.fn(),
    debug: vi.fn(),
  },
}));

describe('TrustedHooksManager', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    vi.mocked(Storage.getGlobalGeminiDir).mockReturnValue('/mock/home/.gemini');
  });

  describe('initialization', () => {
    it('should load existing trusted hooks', () => {
      const existingData = {
        '/project/a': ['hook1:cmd1'],
      };
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existingData));

      const manager = new TrustedHooksManager();
      const untrusted = manager.getUntrustedHooks('/project/a', {
        [HookEventName.BeforeTool]: [
          {
            hooks: [{ type: HookType.Command, command: 'cmd1', name: 'hook1' }],
          },
        ],
      });

      expect(untrusted).toHaveLength(0);
    });

    it('should handle missing config file', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);

      const manager = new TrustedHooksManager();
      const untrusted = manager.getUntrustedHooks('/project/a', {
        [HookEventName.BeforeTool]: [
          {
            hooks: [{ type: HookType.Command, command: 'cmd1', name: 'hook1' }],
          },
        ],
      });

      expect(untrusted).toEqual(['hook1']);
    });
  });

  describe('getUntrustedHooks', () => {
    it('should return names of untrusted hooks', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      const manager = new TrustedHooksManager();

      const projectHooks = {
        [HookEventName.BeforeTool]: [
          {
            hooks: [
              {
                name: 'trusted-hook',
                type: HookType.Command,
                command: 'cmd1',
              } as const,
              {
                name: 'new-hook',
                type: HookType.Command,
                command: 'cmd2',
              } as const,
            ],
          },
        ],
      };

      // Initially both are untrusted
      expect(manager.getUntrustedHooks('/project', projectHooks)).toEqual([
        'trusted-hook',
        'new-hook',
      ]);

      // Trust one
      manager.trustHooks('/project', {
        [HookEventName.BeforeTool]: [
          {
            hooks: [
              {
                name: 'trusted-hook',
                type: HookType.Command,
                command: 'cmd1',
              } as const,
            ],
          },
        ],
      });

      // Only the other one is untrusted
      expect(manager.getUntrustedHooks('/project', projectHooks)).toEqual([
        'new-hook',
      ]);
    });

    it('should use command if name is missing', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      const manager = new TrustedHooksManager();

      const projectHooks = {
        [HookEventName.BeforeTool]: [
          {
            hooks: [{ type: HookType.Command, command: './script.sh' }],
          },
        ],
      };

      expect(
        manager.getUntrustedHooks(
          '/project',
          projectHooks as Partial<Record<HookEventName, HookDefinition[]>>,
        ),
      ).toEqual(['./script.sh']);
    });

    it('should detect change in command as untrusted', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      const manager = new TrustedHooksManager();

      const originalHook = {
        [HookEventName.BeforeTool]: [
          {
            hooks: [
              { name: 'my-hook', type: HookType.Command, command: 'old-cmd' },
            ],
          },
        ],
      };
      const updatedHook = {
        [HookEventName.BeforeTool]: [
          {
            hooks: [
              { name: 'my-hook', type: HookType.Command, command: 'new-cmd' },
            ],
          },
        ],
      };

      manager.trustHooks(
        '/project',
        originalHook as Partial<Record<HookEventName, HookDefinition[]>>,
      );

      expect(
        manager.getUntrustedHooks(
          '/project',
          updatedHook as Partial<Record<HookEventName, HookDefinition[]>>,
        ),
      ).toEqual(['my-hook']);
    });
  });

  describe('persistence', () => {
    it('should save to file when trusting hooks', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      const manager = new TrustedHooksManager();

      manager.trustHooks('/project', {
        [HookEventName.BeforeTool]: [
          {
            hooks: [{ name: 'hook1', type: HookType.Command, command: 'cmd1' }],
          },
        ],
      });

      expect(fs.writeFileSync).toHaveBeenCalledWith(
        expect.stringContaining('trusted_hooks.json'),
        expect.stringContaining('hook1:cmd1'),
      );
    });

    it('should create directory if missing on save', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      const manager = new TrustedHooksManager();

      manager.trustHooks('/project', {});

      expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), {
        recursive: true,
      });
    });
  });
});
