import { NotFoundException } from '@nestjs/common';
import { GetStudentBasicTool } from './get-student-basic.tool';
import { CaslAbilityFactory } from '../../authorization/casl-ability.factory';
import { StudentAccessScopeFactory } from '../../students/student-access-scope.factory';
import { AgentToolContextFactory } from '../agent-tool.types';
import type { AgentToolContext } from '../agent-tool.types';
import type { AuthenticatedUser } from '../../authorization';

const abilityFactory = new CaslAbilityFactory();
const scopeFactory = new StudentAccessScopeFactory(abilityFactory);

function makeCtx(overrides: Partial<AuthenticatedUser> & { id: number; username: string }): AgentToolContext {
  const user: AuthenticatedUser = {
    id: overrides.id,
    username: overrides.username,
    permissions: overrides.permissions ?? [],
    isSuperAdmin: overrides.isSuperAdmin ?? false,
    roles: overrides.roles ?? [],
  };
  return AgentToolContextFactory.fromAuthenticatedUser(user);
}

const studentViewerCtx = makeCtx({ id: 2, username: 'teacher', permissions: ['student:view'] });
const superAdminCtx = makeCtx({ id: 1, username: 'admin', isSuperAdmin: true });
const classEditorCtx = makeCtx({
  id: 4,
  username: 'class_editor',
  permissions: ['student:view', 'class:edit'],
});

function makeTool(svcOverride?: { agentGetStudentBasic: jest.Mock }): GetStudentBasicTool {
  const svc = svcOverride ?? { agentGetStudentBasic: jest.fn().mockResolvedValue(null) };
  return new GetStudentBasicTool(svc as never, scopeFactory);
}

const basicOutput = {
  id: 1,
  name: '张三',
  studentNo: 'S001',
  gender: '男',
  status: 'active',
  organizationId: 10,
  organizationName: '杭州校区',
  classIds: [5],
};

describe('GetStudentBasicTool', () => {
  it('has name "get_student_basic"', () => {
    const tool = makeTool();
    expect(tool.name).toBe('get_student_basic');
  });

  it('requires permission "student:view"', () => {
    const tool = makeTool();
    expect(tool.requiredPermission).toBe('student:view');
  });

  // -----------------------------------------------------------------------
  //  Validation
  // -----------------------------------------------------------------------

  describe('validate', () => {
    it('accepts valid studentId', () => {
      const tool = makeTool();
      const result = tool.validate({ studentId: 1 });
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.value.studentId).toBe(1);
    });

    it('rejects missing studentId', () => {
      const tool = makeTool();
      const result = tool.validate({});
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error).toContain('studentId');
    });

    it('rejects non-integer studentId', () => {
      const tool = makeTool();
      const result = tool.validate({ studentId: 'abc' });
      expect(result.ok).toBe(false);
    });

    it('rejects extra unknown fields', () => {
      const tool = makeTool();
      const result = tool.validate({ studentId: 1, extraField: 'hack' });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error).toContain('extraField');
    });

    it('rejects userId', () => {
      const tool = makeTool();
      const result = tool.validate({ studentId: 1, userId: 999 });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error).toContain('userId');
    });
  });

  // -----------------------------------------------------------------------
  //  P2-2: Scope construction
  // -----------------------------------------------------------------------

  describe('P2-2: scope', () => {
    it('super admin uses manageAll scope', async () => {
      const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) };
      const tool = makeTool(mockSvc);

      await tool.execute({ studentId: 1 }, superAdminCtx);

      expect(mockSvc.agentGetStudentBasic).toHaveBeenCalledWith(
        { type: 'manageAll' },
        1,
      );
    });

    it('non-admin uses teacher scope', async () => {
      const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) };
      const tool = makeTool(mockSvc);

      await tool.execute({ studentId: 1 }, studentViewerCtx);

      expect(mockSvc.agentGetStudentBasic).toHaveBeenCalledWith(
        { type: 'teacher', userId: 2 },
        1,
      );
    });

    it('class:edit uses manageAll scope', async () => {
      const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) };
      const tool = makeTool(mockSvc);

      await tool.execute({ studentId: 1 }, classEditorCtx);

      expect(mockSvc.agentGetStudentBasic).toHaveBeenCalledWith(
        { type: 'manageAll' },
        1,
      );
    });
  });

  // -----------------------------------------------------------------------
  //  P2-1: NotFoundException for null result
  // -----------------------------------------------------------------------

  describe('P2-1: NotFoundException', () => {
    it('null from service throws NotFoundException (not returned as success)', async () => {
      const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(null) };
      const tool = makeTool(mockSvc);

      await expect(tool.execute({ studentId: 999 }, studentViewerCtx)).rejects.toThrow(
        NotFoundException,
      );
    });

    it('service NotFound message is "记录不存在或无权访问"', async () => {
      const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(null) };
      const tool = makeTool(mockSvc);

      await expect(tool.execute({ studentId: 999 }, studentViewerCtx)).rejects.toThrow(
        '记录不存在或无权访问',
      );
    });
  });

  // -----------------------------------------------------------------------
  //  Execute — happy path
  // -----------------------------------------------------------------------

  describe('execute', () => {
    it('returns formatted student data', async () => {
      const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) };
      const tool = makeTool(mockSvc);

      const result = await tool.execute({ studentId: 1 }, superAdminCtx);
      expect(result).toEqual(basicOutput);
      const keys = Object.keys(result as Record<string, unknown>);
      expect(keys).not.toContain('phone');
      expect(keys).not.toContain('idNumber');
    });
  });
});
