import { describe, it, expect, beforeEach, vi } from "vitest";
import type { IamClient } from "../../src/clients/iam.client.js";
import type { MsgClient } from "../../src/clients/msg.client.js";
import type { DataAnaClient } from "../../src/clients/data-ana.client.js";
import type { CoreEduClient } from "../../src/clients/core-edu.client.js";
import type {
  ChildDto,
  UserInfoDto,
  ViewportDto,
} from "../../src/clients/dtos.js";

// Mock safeRedis 以控制 ChildGuard 缓存行为
vi.mock("../../src/shared/cache/redis.client.js", () => ({
  safeRedis: vi.fn().mockResolvedValue(null),
  getRedisClient: vi.fn(() => null),
  closeRedisClient: vi.fn(),
}));

import { buildSelectChildMutationResolver } from "../../src/graphql/resolvers/select-child.resolver.js";
import type { ResolverDeps } from "../../src/graphql/resolvers/index.js";
import type { GraphqlContext } from "../../src/graphql/context.js";
import { ChildGuard } from "../../src/aggregation/child-guard.js";
import { ChildNotBoundError } from "../../src/shared/errors/application-error.js";
import { logger } from "../../src/shared/observability/logger.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";

const mockSafeRedis = vi.mocked(safeRedis);

const MOCK_CHILDREN: ChildDto[] = [
  {
    id: "student-001",
    name: "李同学",
    grade: "三年级",
    classId: "class-001",
    className: "三年级1班",
    gradeId: "grade-003",
  },
  {
    id: "student-002",
    name: "李妹妹",
    grade: "一年级",
    classId: "class-002",
    className: "一年级2班",
    gradeId: "grade-001",
  },
];

class StubIamClient implements IamClient {
  async getUserInfo(userId: string): Promise<UserInfoDto> {
    return {
      id: userId,
      email: "",
      name: "",
      roles: ["parent"],
      permissions: [],
    };
  }
  async getChildrenByParent(_parentId: string): Promise<ChildDto[]> {
    return MOCK_CHILDREN.map((c) => ({ ...c }));
  }
  async getViewports(_userId: string): Promise<ViewportDto[]> {
    return [];
  }
  async getEffectivePermissions(_userId: string): Promise<string[]> {
    return [];
  }
}

function buildMockContext(parentId: string, traceId: string): GraphqlContext {
  return {
    session: {
      parentId,
      roles: ["parent"],
      dataScope: "CHILDREN",
      traceId,
    },
    req: {} as never,
    res: {} as never,
    loaders: {} as never,
  };
}

describe("测试用例 8：selectChild 审计日志（Integration）", () => {
  let iamClient: StubIamClient;
  let childGuard: ChildGuard;
  let loggerInfoSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockSafeRedis.mockResolvedValue(null);
    iamClient = new StubIamClient();
    childGuard = new ChildGuard(iamClient);
    loggerInfoSpy = vi.spyOn(logger, "info").mockImplementation(() => logger);
  });

  it("合法 childId：记录审计日志（traceId + parentId + childId + timestamp）", async () => {
    // safeRedis: miss → set ok（ChildGuard.getBoundChildren）
    mockSafeRedis
      .mockResolvedValueOnce(null) // get → miss
      .mockResolvedValueOnce(undefined); // set → ok

    const deps: ResolverDeps = {
      iamClient,
      coreEduClient: {} as unknown as CoreEduClient,
      dataAnaClient: {} as unknown as DataAnaClient,
      msgClient: {} as unknown as MsgClient,
      childGuard,
    };

    const resolver = buildSelectChildMutationResolver(deps);
    const ctx = buildMockContext("parent-001", "trace-audit-001");

    const result = await resolver(null, { childId: "student-001" }, ctx);

    // 返回值正确
    expect(result.childId).toBe("student-001");
    expect(result.audited).toBe(true);
    expect(result.selectedAt).toBeTruthy();

    // selectedAt 是合法 ISO 时间
    const selectedAtDate = new Date(result.selectedAt);
    expect(Number.isNaN(selectedAtDate.getTime())).toBe(false);

    // 审计日志被调用
    expect(loggerInfoSpy).toHaveBeenCalled();

    // 查找 "Child selected (audit log)" 的调用
    const auditCall = loggerInfoSpy.mock.calls.find(
      (call) => typeof call[1] === "string" && call[1].includes("audit log"),
    );
    expect(auditCall).toBeDefined();

    // 审计日志包含 traceId + parentId + childId + selectedAt
    const logPayload = auditCall![0] as Record<string, unknown>;
    expect(logPayload.parentId).toBe("parent-001");
    expect(logPayload.childId).toBe("student-001");
    expect(logPayload.traceId).toBe("trace-audit-001");
    expect(logPayload.selectedAt).toBe(result.selectedAt);
  });

  it("越权 childId：先抛 ChildNotBoundError，不记录审计日志", async () => {
    // safeRedis: miss → set ok（ChildGuard.getBoundChildren 第一次调用）
    mockSafeRedis
      .mockResolvedValueOnce(null) // get → miss
      .mockResolvedValueOnce(undefined); // set → ok

    const deps: ResolverDeps = {
      iamClient,
      coreEduClient: {} as unknown as CoreEduClient,
      dataAnaClient: {} as unknown as DataAnaClient,
      msgClient: {} as unknown as MsgClient,
      childGuard,
    };

    const resolver = buildSelectChildMutationResolver(deps);
    const ctx = buildMockContext("parent-001", "trace-audit-002");

    // 越权 childId 抛 ChildNotBoundError
    await expect(
      resolver(null, { childId: "student-999" }, ctx),
    ).rejects.toThrow(ChildNotBoundError);

    // 不应记录审计日志（只可能有 ChildGuard 的 warn 日志）
    const auditCall = loggerInfoSpy.mock.calls.find(
      (call) => typeof call[1] === "string" && call[1].includes("audit log"),
    );
    expect(auditCall).toBeUndefined();
  });

  it("审计日志的 selectedAt 与返回值一致", async () => {
    mockSafeRedis.mockResolvedValueOnce(null).mockResolvedValueOnce(undefined);

    const deps: ResolverDeps = {
      iamClient,
      coreEduClient: {} as unknown as CoreEduClient,
      dataAnaClient: {} as unknown as DataAnaClient,
      msgClient: {} as unknown as MsgClient,
      childGuard,
    };

    const resolver = buildSelectChildMutationResolver(deps);
    const ctx = buildMockContext("parent-001", "trace-audit-003");

    const result = await resolver(null, { childId: "student-002" }, ctx);

    const auditCall = loggerInfoSpy.mock.calls.find(
      (call) => typeof call[1] === "string" && call[1].includes("audit log"),
    );
    expect(auditCall).toBeDefined();

    const logPayload = auditCall![0] as Record<string, unknown>;
    // 审计日志的 selectedAt 与返回值完全一致
    expect(logPayload.selectedAt).toBe(result.selectedAt);
    // childId 也一致
    expect(logPayload.childId).toBe(result.childId);
  });

  it("不同 traceId 生成不同审计日志", async () => {
    // 第一次调用
    mockSafeRedis.mockResolvedValueOnce(null).mockResolvedValueOnce(undefined);

    const deps: ResolverDeps = {
      iamClient,
      coreEduClient: {} as unknown as CoreEduClient,
      dataAnaClient: {} as unknown as DataAnaClient,
      msgClient: {} as unknown as MsgClient,
      childGuard,
    };

    const resolver = buildSelectChildMutationResolver(deps);

    // 第一次：traceId-A
    const result1 = await resolver(
      null,
      { childId: "student-001" },
      buildMockContext("parent-001", "traceId-A"),
    );

    // 等待 2ms 确保 selectedAt 时间戳不同
    await new Promise((r) => setTimeout(r, 2));

    // 第二次：traceId-B（缓存已写入，直接命中或重新调）
    mockSafeRedis.mockResolvedValueOnce(null).mockResolvedValueOnce(undefined);

    const result2 = await resolver(
      null,
      { childId: "student-002" },
      buildMockContext("parent-002", "traceId-B"),
    );

    // 两次审计日志的 traceId 不同
    const auditCalls = loggerInfoSpy.mock.calls.filter(
      (call) => typeof call[1] === "string" && call[1].includes("audit log"),
    );
    expect(auditCalls).toHaveLength(2);

    const payload1 = auditCalls[0]![0] as Record<string, unknown>;
    const payload2 = auditCalls[1]![0] as Record<string, unknown>;
    expect(payload1.traceId).toBe("traceId-A");
    expect(payload2.traceId).toBe("traceId-B");
    expect(payload1.childId).toBe("student-001");
    expect(payload2.childId).toBe("student-002");
    expect(result1.selectedAt).not.toBe(result2.selectedAt);
  });
});
