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

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ConsecaSafetyChecker } from './conseca.js';
import { SafetyCheckDecision, type SafetyCheckInput } from '../protocol.js';
import {
  logConsecaPolicyGeneration,
  logConsecaVerdict,
} from '../../telemetry/index.js';
import type { Config } from '../../config/config.js';
import * as policyGenerator from './policy-generator.js';
import * as policyEnforcer from './policy-enforcer.js';

vi.mock('../../telemetry/index.js', () => ({
  logConsecaPolicyGeneration: vi.fn(),
  ConsecaPolicyGenerationEvent: vi.fn(),
  logConsecaVerdict: vi.fn(),
  ConsecaVerdictEvent: vi.fn(),
}));

vi.mock('./policy-generator.js');
vi.mock('./policy-enforcer.js');

describe('ConsecaSafetyChecker', () => {
  let checker: ConsecaSafetyChecker;
  let mockConfig: Config;

  beforeEach(() => {
    // Reset singleton instance to ensure clean state
    ConsecaSafetyChecker.resetInstance();
    // Get the fresh singleton instance
    checker = ConsecaSafetyChecker.getInstance();

    mockConfig = {
      get config() {
        return this;
      },
      enableConseca: true,
      getToolRegistry: vi.fn().mockReturnValue({
        getFunctionDeclarations: vi.fn().mockReturnValue([]),
      }),
    } as unknown as Config;
    checker.setContext(mockConfig);
    vi.clearAllMocks();

    // Default mock implementations
    vi.mocked(policyGenerator.generatePolicy).mockResolvedValue({ policy: {} });
    vi.mocked(policyEnforcer.enforcePolicy).mockResolvedValue({
      decision: SafetyCheckDecision.ALLOW,
    });
  });

  it('should be a singleton', () => {
    const instance1 = ConsecaSafetyChecker.getInstance();
    const instance2 = ConsecaSafetyChecker.getInstance();
    expect(instance1).toBe(instance2);
  });

  it('should return ALLOW when no user prompt is present in context', async () => {
    const input: SafetyCheckInput = {
      protocolVersion: '1.0.0',
      toolCall: { name: 'testTool' },
      context: {
        environment: { cwd: '/tmp', workspaces: [] },
      },
    };

    const result = await checker.check(input);
    expect(result.decision).toBe(SafetyCheckDecision.ALLOW);
  });

  it('should return ALLOW if enableConseca is false', async () => {
    const disabledConfig = {
      get config() {
        return this;
      },
      enableConseca: false,
    } as unknown as Config;
    checker.setContext(disabledConfig);

    const input: SafetyCheckInput = {
      protocolVersion: '1.0.0',
      toolCall: { name: 'testTool' },
      context: {
        environment: { cwd: '/tmp', workspaces: [] },
      },
    };

    const result = await checker.check(input);
    expect(result.decision).toBe(SafetyCheckDecision.ALLOW);
    expect(result.reason).toBe('Conseca is disabled');
    expect(policyGenerator.generatePolicy).not.toHaveBeenCalled();
  });

  it('getPolicy should return cached policy if user prompt matches', async () => {
    const mockPolicy = {
      tool: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    vi.mocked(policyGenerator.generatePolicy).mockResolvedValue({
      policy: mockPolicy,
    });

    const policy1 = await checker.getPolicy('prompt', 'trusted', mockConfig);
    const policy2 = await checker.getPolicy('prompt', 'trusted', mockConfig);

    expect(policy1).toBe(mockPolicy);
    expect(policy2).toBe(mockPolicy);
    expect(policyGenerator.generatePolicy).toHaveBeenCalledTimes(1);
  });

  it('getPolicy should generate new policy if user prompt changes', async () => {
    const mockPolicy1 = {
      tool1: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    const mockPolicy2 = {
      tool2: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    vi.mocked(policyGenerator.generatePolicy)
      .mockResolvedValueOnce({ policy: mockPolicy1 })
      .mockResolvedValueOnce({ policy: mockPolicy2 });

    const policy1 = await checker.getPolicy('prompt1', 'trusted', mockConfig);
    const policy2 = await checker.getPolicy('prompt2', 'trusted', mockConfig);

    expect(policy1).toBe(mockPolicy1);
    expect(policy2).toBe(mockPolicy2);
    expect(policyGenerator.generatePolicy).toHaveBeenCalledTimes(2);
  });

  it('check should call getPolicy and enforcePolicy', async () => {
    const mockPolicy = {
      tool: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    vi.mocked(policyGenerator.generatePolicy).mockResolvedValue({
      policy: mockPolicy,
    });
    vi.mocked(policyEnforcer.enforcePolicy).mockResolvedValue({
      decision: SafetyCheckDecision.ALLOW,
    });

    const input: SafetyCheckInput = {
      protocolVersion: '1.0.0',
      toolCall: { name: 'tool', args: {} },
      context: {
        environment: { cwd: '.', workspaces: [] },
        history: {
          turns: [
            {
              user: { text: 'user prompt' },
              model: {},
            },
          ],
        },
      },
    };

    const result = await checker.check(input);

    expect(policyGenerator.generatePolicy).toHaveBeenCalledWith(
      'user prompt',
      expect.any(String),
      mockConfig,
    );
    expect(policyEnforcer.enforcePolicy).toHaveBeenCalledWith(
      mockPolicy,
      input.toolCall,
      mockConfig,
    );
    expect(result.decision).toBe(SafetyCheckDecision.ALLOW);
  });

  it('check should return ALLOW if no user prompt found (fallback)', async () => {
    const input: SafetyCheckInput = {
      protocolVersion: '1.0.0',
      toolCall: { name: 'tool', args: {} },
      context: {
        environment: { cwd: '.', workspaces: [] },
      },
    };

    const result = await checker.check(input);

    expect(policyGenerator.generatePolicy).not.toHaveBeenCalled();
    expect(result.decision).toBe(SafetyCheckDecision.ALLOW);
  });

  // Test state helpers
  it('should expose current state via helpers', async () => {
    const mockPolicy = {
      tool: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    vi.mocked(policyGenerator.generatePolicy).mockResolvedValue({
      policy: mockPolicy,
    });

    await checker.getPolicy('prompt', 'trusted', mockConfig);

    expect(checker.getCurrentPolicy()).toBe(mockPolicy);
    expect(checker.getActiveUserPrompt()).toBe('prompt');
  });
  it('should log policy generation event when config is set', async () => {
    const mockPolicy = {
      tool: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    vi.mocked(policyGenerator.generatePolicy).mockResolvedValue({
      policy: mockPolicy,
    });

    await checker.getPolicy('telemetry_prompt', 'trusted', mockConfig);

    expect(logConsecaPolicyGeneration).toHaveBeenCalledWith(
      mockConfig,
      expect.anything(),
    );
  });

  it('should log verdict event on check', async () => {
    const mockPolicy = {
      tool: {
        permissions: SafetyCheckDecision.ALLOW,
        constraints: 'None',
        rationale: 'Test',
      },
    };
    vi.mocked(policyGenerator.generatePolicy).mockResolvedValue({
      policy: mockPolicy,
    });
    vi.mocked(policyEnforcer.enforcePolicy).mockResolvedValue({
      decision: SafetyCheckDecision.ALLOW,
      reason: 'Allowed by policy',
    });

    const input: SafetyCheckInput = {
      protocolVersion: '1.0.0',
      toolCall: { name: 'tool', args: {} },
      context: {
        environment: { cwd: '.', workspaces: [] },
        history: {
          turns: [
            {
              user: { text: 'user prompt' },
              model: {},
            },
          ],
        },
      },
    };

    await checker.check(input);

    expect(logConsecaVerdict).toHaveBeenCalledWith(
      mockConfig,
      expect.anything(),
    );
  });
});
