import { NotFoundException } from '@nestjs/common';
import { CaslAbilityFactory } from '../authorization/casl-ability.factory';
import { AuthorizationService } from '../authorization/authorization.service';
import { AgentToolRegistry } from './agent-tool.registry';
import { AgentToolExecutor } from './agent-tool.executor';
import { AgentToolContextFactory, AgentToolContext } from './agent-tool.types';
import type { ToolDef, ToolInputResult, ToolDescriptor } from './agent-tool.types';
import type { AuthenticatedUser } from '../authorization';

// ---------------------------------------------------------------------------
//  Helpers
// ---------------------------------------------------------------------------

const abilityFactory = new CaslAbilityFactory();

/** Create context via the factory (the ONLY valid path). */
function makeCtx(user: Partial<AuthenticatedUser> & { id: number; username: string }): AgentToolContext {
  const u: AuthenticatedUser = {
    id: user.id,
    username: user.username,
    permissions: user.permissions ?? [],
    isSuperAdmin: user.isSuperAdmin ?? false,
    roles: user.roles ?? [],
  };
  return AgentToolContextFactory.fromAuthenticatedUser(u);
}

const superAdminCtx = makeCtx({ id: 1, username: 'admin', isSuperAdmin: true });
const studentViewerCtx = makeCtx({
  id: 2,
  username: 'teacher_zhang',
  permissions: ['student:view'],
});
const noPermCtx = makeCtx({ id: 3, username: 'guest', permissions: [] });

/** Create a simple mock tool. */
function makeTool(overrides: Partial<ToolDef> = {}): ToolDef {
  return {
    name: 'echo',
    description: 'echoes input',
    requiredPermission: 'student:view',
    inputSchema: { type: 'object', properties: { text: { type: 'string' } }, additionalProperties: false },
    validate(input: Record<string, unknown>): ToolInputResult<Record<string, unknown>> {
      const forbidden = new Set(['userId', 'isSuperAdmin', 'permissions', 'roles', 'ability']);
      for (const key of Object.keys(input)) {
        if (forbidden.has(key)) return { ok: false, error: `禁止字段: ${key}` };
      }
      return { ok: true, value: input };
    },
    async execute(input: Record<string, unknown>): Promise<unknown> {
      return { echoed: input };
    },
    ...overrides,
  };
}

function makeExecutor(opLogMock?: { log: jest.Mock }): {
  executor: AgentToolExecutor;
  registry: AgentToolRegistry;
  opLog: { log: jest.Mock };
} {
  const authz = new AuthorizationService(abilityFactory);
  const registry = new AgentToolRegistry();
  const opLog = opLogMock ?? { log: jest.fn().mockResolvedValue(undefined) };
  const executor = new AgentToolExecutor(registry, abilityFactory, authz, opLog as never);
  return { executor, registry, opLog };
}

// ---------------------------------------------------------------------------
//  Fix 2: AgentToolContext — immutability & forgery resistance
// ---------------------------------------------------------------------------

describe('AgentToolContext — immutability & forgery resistance', () => {
  it('context is fully frozen (cannot add/remove/modify properties)', () => {
    const ctx = AgentToolContextFactory.fromAuthenticatedUser({
      id: 1, username: 'admin', permissions: ['student:view'], isSuperAdmin: false, roles: [],
    });

    // Frozen object throws in strict mode on mutation
    expect(() => {
      (ctx as Record<string, unknown>).isSuperAdmin = true;
    }).toThrow();
    expect(() => {
      (ctx as Record<string, unknown>).userId = 999;
    }).toThrow();
    expect(() => {
      (ctx as Record<string, unknown>).newField = 'injected';
    }).toThrow();
  });

  it('permissions array is frozen (cannot push/splice)', () => {
    const ctx = AgentToolContextFactory.fromAuthenticatedUser({
      id: 1, username: 'admin', permissions: ['student:view'], isSuperAdmin: false, roles: [],
    });
    expect(() => {
      (ctx.permissions as string[]).push('student:delete');
    }).toThrow();
  });

  it('mutating original AuthenticatedUser does NOT affect context', () => {
    const user: AuthenticatedUser = {
      id: 1, username: 'admin', permissions: ['student:view'], isSuperAdmin: false, roles: [],
    };
    const ctx = AgentToolContextFactory.fromAuthenticatedUser(user);

    user.permissions.push('superadmin:hack');
    user.isSuperAdmin = true;

    expect(ctx.permissions).toEqual(['student:view']);
    expect(ctx.isSuperAdmin).toBe(false);
  });

  it('hand-crafted plain-object context is rejected by executor.execute', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const fakeCtx = {
      userId: 1,
      username: 'hacker',
      permissions: Object.freeze(['student:view', 'student:delete']),
      isSuperAdmin: true,
    } as AgentToolContext;

    const result = await executor.execute('echo', { text: 'hi' }, fakeCtx);
    expect(result.status).toBe('denied');
    expect(result.error).toBe('权限不足');
    expect(result.result).toBeUndefined();
  });

  it('hand-crafted plain-object context is rejected by executor.listAvailable', () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const fakeCtx = {
      userId: 1,
      username: 'hacker',
      permissions: Object.freeze(['student:delete']),
      isSuperAdmin: true,
    } as AgentToolContext;

    expect(() => executor.listAvailable(fakeCtx)).toThrow('DENIED');
  });

  it('Object.create(prototype) without factory is rejected', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    // Even if you get the prototype right, it's not in the WeakSet
    const fakeCtx2 = Object.create(AgentToolContext.prototype) as AgentToolContext;
    Object.defineProperties(fakeCtx2, {
      userId: { value: 1 },
      username: { value: 'hacker' },
      permissions: { value: Object.freeze(['student:view', 'student:delete']) },
      isSuperAdmin: { value: true },
    });
    Object.freeze(fakeCtx2);

    const result = await executor.execute('echo', { text: 'hi' }, fakeCtx2);
    expect(result.status).toBe('denied');
  });

  it('context does NOT expose ability field', () => {
    const ctx = superAdminCtx;
    expect((ctx as Record<string, unknown>).ability).toBeUndefined();
  });

  it('passing superAdmin-like permissions on non-superAdmin user does NOT grant superAdmin', () => {
    const ctx = makeCtx({ id: 5, username: 'fake', permissions: ['superadmin:all'], isSuperAdmin: false });
    expect(ctx.isSuperAdmin).toBe(false);

    const ability = abilityFactory.createForUser({
      permissions: ctx.permissions,
      isSuperAdmin: ctx.isSuperAdmin,
    });
    expect(ability.can('manage', 'all')).toBe(false);
  });
});

// ---------------------------------------------------------------------------
//  Fix 1: listAvailable via Executor (not Registry)
// ---------------------------------------------------------------------------

describe('listAvailable via Executor', () => {
  it('returns ToolDescriptors with name, description, inputSchema — but NOT execute/validate', () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const tools: ToolDescriptor[] = executor.listAvailable(studentViewerCtx);
    expect(tools).toHaveLength(1);
    expect(tools[0].name).toBe('student_search');
    expect(tools[0].description).toBeTruthy();
    expect(tools[0].inputSchema).toBeDefined();
    expect((tools[0] as Record<string, unknown>).execute).toBeUndefined();
    expect((tools[0] as Record<string, unknown>).validate).toBeUndefined();
    expect((tools[0] as Record<string, unknown>).requiredPermission).toBeUndefined();
  });

  it('super admin sees all tools', () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 't1', requiredPermission: 'student:view' }));
    registry.register(makeTool({ name: 't2', requiredPermission: 'bill:export' }));

    const tools = executor.listAvailable(superAdminCtx);
    expect(tools).toHaveLength(2);
  });

  it('hides tool when principal lacks required permission', () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const tools = executor.listAvailable(noPermCtx);
    expect(tools).toHaveLength(0);
  });

  it('filters by exact permission code', () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));
    registry.register(makeTool({ name: 'bill_export', requiredPermission: 'bill:export-excel' }));

    const tools = executor.listAvailable(studentViewerCtx);
    expect(tools).toHaveLength(1);
    expect(tools[0].name).toBe('student_search');
  });

  it('descriptors include inputSchema when present', () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'get_student',
        requiredPermission: 'student:view',
        inputSchema: { type: 'object', properties: { studentId: { type: 'integer' } }, required: ['studentId'], additionalProperties: false },
      }),
    );

    const tools = executor.listAvailable(studentViewerCtx);
    expect(tools[0].inputSchema).toBeDefined();
    expect(tools[0].inputSchema!.required).toContain('studentId');
  });
});

// ---------------------------------------------------------------------------
//  rawInput type guards
// ---------------------------------------------------------------------------

describe('rawInput type guards', () => {
  it('rejects null input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const result = await executor.execute('echo', null, studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });

  it('rejects array input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const result = await executor.execute('echo', [1, 2, 3], studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });

  it('rejects string input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const result = await executor.execute('echo', 'just a string', studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });

  it('rejects number input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const result = await executor.execute('echo', 42, studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });

  it('validator that throws is caught and returns safe error', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'crash_validate',
        requiredPermission: 'student:view',
        validate(): never {
          throw new Error('INTERNAL: validator crashed with raw SQL');
        },
      }),
    );

    const result = await executor.execute('crash_validate', { x: 1 }, studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
    expect(result.error).not.toContain('SQL');
    expect(result.error).not.toContain('INTERNAL');
  });
});

// ---------------------------------------------------------------------------
//  Error & audit sanitization
// ---------------------------------------------------------------------------

describe('Error & audit sanitization', () => {
  it('tool throw with phone/SQL in message does NOT leak to result', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'leaky',
        requiredPermission: 'student:view',
        async execute(): Promise<unknown> {
          throw new Error('phone=13800138000, idNumber=320106199001011234, SQL: SELECT * FROM students WHERE id=1');
        },
      }),
    );

    const result = await executor.execute('leaky', {}, studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('工具执行失败');
    expect(result.error).not.toContain('13800138000');
    expect(result.error).not.toContain('320106');
    expect(result.error).not.toContain('SQL');
    expect(result.error).not.toContain('SELECT');
  });

  it('malicious tool name is sanitized in audit action', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor } = makeExecutor(opLog);

    const result = await executor.execute(
      'evil\n<script>alert(1)</script>!@#$%^&*()very_long_name_exceeding_64_chars_padding_padding_padding_padding_END',
      {},
      studentViewerCtx,
    );

    expect(result.toolName).not.toContain('\n');
    expect(result.toolName).not.toContain('<script>');
    expect(result.toolName).not.toContain('!');
    expect(result.toolName.length).toBeLessThanOrEqual(64);

    expect(opLog.log).toHaveBeenCalled();
    const call = opLog.log.mock.calls[0][0];
    expect(call.action).not.toContain('\n');
    expect(call.action).not.toContain('<script>');
    expect(call.action).not.toContain('!');
    expect(call.action).toContain('denied');
  });

  it('audit detail never contains exception messages', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(
      makeTool({
        name: 'crash',
        requiredPermission: 'student:view',
        async execute(): Promise<unknown> {
          throw new Error('DB error: table students at 10.0.0.1:5432');
        },
      }),
    );

    await executor.execute('crash', {}, studentViewerCtx);
    const call = opLog.log.mock.calls[0][0];
    expect(call.detail).toBe('执行失败');
    expect(call.detail).not.toContain('DB error');
    expect(call.detail).not.toContain('10.0.0.1');
    expect(call.detail).not.toContain('5432');
  });

  it('unknown tool returns generic denied, not raw tool name detail', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor } = makeExecutor(opLog);

    const result = await executor.execute('hack_tool_with_pii_13800138000', {}, studentViewerCtx);
    expect(result.error).toBe('未知工具');
    expect(result.error).not.toContain('13800138000');

    const call = opLog.log.mock.calls[0][0];
    expect(call.detail).toBe('拒绝访问');
    expect(call.detail).not.toContain('13800138000');
  });
});

// ---------------------------------------------------------------------------
//  NotFoundException → not_found
// ---------------------------------------------------------------------------

describe('NotFoundException → not_found', () => {
  it('NotFound from tool returns not_found with safe message', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'find_student',
        requiredPermission: 'student:view',
        async execute(): Promise<unknown> {
          throw new NotFoundException('原始内部消息: student 999 not in scope');
        },
      }),
    );

    const result = await executor.execute('find_student', {}, studentViewerCtx);
    expect(result.status).toBe('not_found');
    expect(result.error).toBe('记录不存在或无权访问');
    expect(result.error).not.toContain('999');
    expect(result.error).not.toContain('原始内部消息');
  });

  it('generic Error from tool returns failed with safe message', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'crash',
        requiredPermission: 'student:view',
        async execute(): Promise<unknown> {
          throw new Error('random runtime error');
        },
      }),
    );

    const result = await executor.execute('crash', {}, studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('工具执行失败');
  });
});

// ---------------------------------------------------------------------------
//  execute — core behavior
// ---------------------------------------------------------------------------

describe('execute — unknown tool', () => {
  it('returns denied for unknown tool name', async () => {
    const { executor } = makeExecutor();

    const result = await executor.execute('nonexistent', {}, superAdminCtx);
    expect(result.status).toBe('denied');
    expect(result.toolName).toBe('nonexistent');
    expect(result.error).toBe('未知工具');
  });
});

describe('execute — double-check authorization', () => {
  it('denies even if tool is registered but principal lacks permission', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const result = await executor.execute('student_search', { q: 'test' }, noPermCtx);
    expect(result.status).toBe('denied');
    expect(result.error).toBe('权限不足');
  });

  it('allows execution when principal has required permission', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const result = await executor.execute('student_search', { q: 'test' }, studentViewerCtx);
    expect(result.status).toBe('success');
  });
});

describe('execute — forged input rejection', () => {
  it('rejects userId in input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const result = await executor.execute(
      'student_search',
      { userId: 999, q: 'test' },
      studentViewerCtx,
    );
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });

  it('rejects isSuperAdmin in input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const result = await executor.execute(
      'student_search',
      { isSuperAdmin: true, q: 'test' },
      studentViewerCtx,
    );
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });

  it('rejects permissions in input', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    const result = await executor.execute(
      'student_search',
      { permissions: ['student:delete'], q: 'test' },
      studentViewerCtx,
    );
    expect(result.status).toBe('failed');
    expect(result.error).toBe('输入参数无效');
  });
});

describe('execute — success', () => {
  it('returns result on success', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'echo',
        requiredPermission: 'student:view',
        async execute(input: Record<string, unknown>): Promise<unknown> {
          return { message: input.text };
        },
      }),
    );

    const result = await executor.execute('echo', { text: 'hello' }, studentViewerCtx);
    expect(result.status).toBe('success');
    expect(result.result).toEqual({ message: 'hello' });
  });
});

describe('execute — tool error handling', () => {
  it('returns failed status with safe message on tool throw', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'crashy',
        requiredPermission: 'student:view',
        async execute(): Promise<unknown> {
          throw new Error('数据库连接失败: connection refused at 10.0.0.1:5432');
        },
      }),
    );

    const result = await executor.execute('crashy', {}, studentViewerCtx);
    expect(result.status).toBe('failed');
    expect(result.error).toBe('工具执行失败');
    expect(result.error).not.toContain('数据库连接失败');
    expect(result.error).not.toContain('10.0.0.1');
  });
});

// ---------------------------------------------------------------------------
//  Fix 3: Audit — awaited, best-effort
// ---------------------------------------------------------------------------

describe('audit logging — awaited best-effort', () => {
  it('logs success with userId/username from context', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    await executor.execute('echo', { text: 'hi' }, studentViewerCtx);

    expect(opLog.log).toHaveBeenCalledTimes(1);
    const call = opLog.log.mock.calls[0][0];
    expect(call.userId).toBe(2);
    expect(call.username).toBe('teacher_zhang');
    expect(call.module).toBe('AI Agent Tool');
    expect(call.action).toContain('echo');
    expect(call.action).toContain('success');
  });

  it('logs denied with status', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    await executor.execute('student_search', {}, noPermCtx);

    const call = opLog.log.mock.calls[0][0];
    expect(call.action).toContain('denied');
    expect(call.detail).toBe('拒绝访问');
  });

  it('logs failed on validation error', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'student_search', requiredPermission: 'student:view' }));

    await executor.execute('student_search', { isSuperAdmin: true }, studentViewerCtx);

    const call = opLog.log.mock.calls[0][0];
    expect(call.action).toContain('failed');
  });

  it('audit detail NEVER contains phone/ID/sensitive fields', async () => {
    const opLog = { log: jest.fn().mockResolvedValue(undefined) };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    await executor.execute('echo', { phone: '13800138000', name: 'test' }, studentViewerCtx);

    const call = opLog.log.mock.calls[0][0];
    const detail = call.detail as string;
    expect(detail).not.toContain('13800138000');
    expect(detail).not.toContain('phone');
    expect(detail).not.toContain('idNumber');
    expect(detail).not.toContain('password');
  });

  it('audit write failure does not break successful tool call', async () => {
    const opLog = { log: jest.fn().mockRejectedValue(new Error('DB write error')) };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const result = await executor.execute('echo', { text: 'hi' }, studentViewerCtx);
    expect(result.status).toBe('success');
    expect(result.result).toEqual({ echoed: { text: 'hi' } });
  });

  it('execute awaits audit before returning (delayed audit does not drop)', async () => {
    let auditResolved = false;
    const opLog = {
      log: jest.fn().mockImplementation(() => {
        return new Promise<void>((resolve) => {
          setTimeout(() => {
            auditResolved = true;
            resolve();
          }, 50);
        });
      }),
    };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    // At call time, audit hasn't resolved
    expect(auditResolved).toBe(false);
    const result = await executor.execute('echo', { text: 'hi' }, studentViewerCtx);
    // After execute returns, audit IS resolved (awaited)
    expect(auditResolved).toBe(true);
    expect(result.status).toBe('success');
  });

  it('audit rejection still resolves execute with correct result', async () => {
    const opLog = {
      log: jest.fn().mockImplementation(() => {
        return new Promise<void>((_, reject) => {
          setTimeout(() => reject(new Error('audit write failed')), 10);
        });
      }),
    };
    const { executor, registry } = makeExecutor(opLog);
    registry.register(makeTool({ name: 'echo', requiredPermission: 'student:view' }));

    const result = await executor.execute('echo', { text: 'hi' }, studentViewerCtx);
    expect(result.status).toBe('success');
    expect(result.result).toEqual({ echoed: { text: 'hi' } });
  });
});

// ---------------------------------------------------------------------------
//  Super admin
// ---------------------------------------------------------------------------

describe('super admin', () => {
  it('super admin can execute any tool regardless of permission', async () => {
    const { executor, registry } = makeExecutor();
    registry.register(
      makeTool({
        name: 'admin_only',
        requiredPermission: 'nuclear:launch',
      }),
    );

    const result = await executor.execute('admin_only', {}, superAdminCtx);
    expect(result.status).toBe('success');
  });

  it('listAvailable returns all tools for super admin', () => {
    const { executor, registry } = makeExecutor();
    registry.register(makeTool({ name: 't1', requiredPermission: 'ghost:action' }));
    registry.register(makeTool({ name: 't2', requiredPermission: 'custom:code' }));

    const tools = executor.listAvailable(superAdminCtx);
    expect(tools).toHaveLength(2);
  });
});
