import { describe, it, expect, beforeEach, vi } from "vitest";
import type { IamClient } from "../../src/clients/iam.client.js";
import type {
  ChildDto,
  UserInfoDto,
  ViewportDto,
} from "../../src/clients/dtos.js";

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

import { ChildGuard } from "../../src/aggregation/child-guard.js";
import { ChildNotBoundError } from "../../src/shared/errors/application-error.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";

const mockSafeRedis = vi.mocked(safeRedis);

/**
 * 可追踪调用次数的 Mock IamClient
 */
class TrackingIamClient implements IamClient {
  getChildrenCallCount = 0;
  private readonly children: ChildDto[];

  constructor(children: ChildDto[]) {
    this.children = children;
  }

  async getUserInfo(userId: string): Promise<UserInfoDto> {
    return {
      id: userId,
      email: "test@example.com",
      name: "测试家长",
      roles: ["parent"],
      permissions: [],
    };
  }

  async getChildrenByParent(_parentId: string): Promise<ChildDto[]> {
    this.getChildrenCallCount++;
    // 小延迟确保并发请求重叠
    await new Promise((r) => setTimeout(r, 10));
    return this.children.map((c) => ({ ...c }));
  }

  async getViewports(_userId: string): Promise<ViewportDto[]> {
    return [];
  }

  async getEffectivePermissions(_userId: string): Promise<string[]> {
    return [];
  }
}

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",
  },
];

describe("ChildGuard", () => {
  let iamClient: TrackingIamClient;
  let childGuard: ChildGuard;

  beforeEach(() => {
    vi.clearAllMocks();
    mockSafeRedis.mockResolvedValue(null);
    iamClient = new TrackingIamClient(MOCK_CHILDREN);
    childGuard = new ChildGuard(iamClient);
  });

  // ============ 测试用例 1：ChildGuard 拦截越权 childId ============
  describe("测试用例 1：拦截越权 childId", () => {
    it("childId 不在绑定列表时抛 ChildNotBoundError(403)", async () => {
      // safeRedis miss → 调 iam
      mockSafeRedis
        .mockResolvedValueOnce(null) // get → miss
        .mockResolvedValueOnce(undefined); // set → ok

      await expect(
        childGuard.validateChildAccess("parent-001", "student-999"),
      ).rejects.toThrow(ChildNotBoundError);

      try {
        await childGuard.validateChildAccess("parent-001", "student-999");
      } catch (err) {
        const error = err as ChildNotBoundError;
        expect(error.statusCode).toBe(403);
        expect(error.code).toBe("BFF_PARENT_CHILD_NOT_BOUND");
        expect(error.details?.parentId).toBe("parent-001");
        expect(error.details?.requestedChildId).toBe("student-999");
      }
    });

    it("childId 在绑定列表时不抛异常", async () => {
      mockSafeRedis
        .mockResolvedValueOnce(null) // get → miss
        .mockResolvedValueOnce(undefined); // set → ok

      await expect(
        childGuard.validateChildAccess("parent-001", "student-001"),
      ).resolves.toBeUndefined();
    });

    it("支持多个绑定的 childId", async () => {
      mockSafeRedis
        .mockResolvedValueOnce(null) // get → miss
        .mockResolvedValueOnce(undefined); // set → ok

      await expect(
        childGuard.validateChildAccess("parent-001", "student-002"),
      ).resolves.toBeUndefined();
    });
  });

  // ============ 测试用例 2：缓存命中 ============
  describe("测试用例 2：缓存命中（30s 内第二次不调 iam）", () => {
    it("Redis 缓存命中时不调用 iam.GetChildrenByParent", async () => {
      const cachedChildren = JSON.stringify(MOCK_CHILDREN);

      // 第一次调用：缓存未命中 → 调 iam → 写缓存
      mockSafeRedis
        .mockResolvedValueOnce(null) // get → miss
        .mockResolvedValueOnce(undefined); // set → ok
      await childGuard.getBoundChildren("parent-001");
      expect(iamClient.getChildrenCallCount).toBe(1);

      // 第二次调用：缓存命中 → 不调 iam
      mockSafeRedis.mockResolvedValueOnce(cachedChildren); // get → hit
      const children = await childGuard.getBoundChildren("parent-001");

      expect(iamClient.getChildrenCallCount).toBe(1); // 仍然只有 1 次
      expect(children).toHaveLength(2);
      expect(children[0]!.id).toBe("student-001");
    });

    it("缓存返回的数据与原始数据一致", async () => {
      const cachedChildren = JSON.stringify(MOCK_CHILDREN);

      mockSafeRedis.mockResolvedValueOnce(cachedChildren);

      const children = await childGuard.getBoundChildren("parent-001");

      expect(children).toHaveLength(2);
      expect(children[0]!.id).toBe("student-001");
      expect(children[0]!.name).toBe("李同学");
      expect(children[1]!.id).toBe("student-002");
    });
  });

  // ============ 测试用例 3：缓存击穿保护（singleflight）============
  describe("测试用例 3：缓存击穿保护（singleflight）", () => {
    it("并发 100 请求只调 iam 1 次", async () => {
      // safeRedis 始终返回 null（缓存未命中）
      mockSafeRedis.mockResolvedValue(null);

      // 并发发起 100 个请求
      const promises: Promise<unknown>[] = [];
      for (let i = 0; i < 100; i++) {
        promises.push(childGuard.getBoundChildren("parent-001"));
      }

      const results = await Promise.all(promises);

      // 所有请求都成功返回
      expect(results).toHaveLength(100);

      // iam 只被调用了 1 次（singleflight 保护）
      expect(iamClient.getChildrenCallCount).toBe(1);

      // 所有结果一致
      const firstResult = results[0] as ChildDto[];
      expect(firstResult).toHaveLength(2);
      for (const result of results) {
        expect(result).toEqual(firstResult);
      }
    });

    it("不同 parentId 的请求各自独立调用 iam", async () => {
      mockSafeRedis.mockResolvedValue(null);

      await Promise.all([
        childGuard.getBoundChildren("parent-001"),
        childGuard.getBoundChildren("parent-002"),
      ]);

      // 不同 parentId 各调 1 次
      expect(iamClient.getChildrenCallCount).toBe(2);
    });

    it("singleflight 完成后 inflight map 被清理", async () => {
      mockSafeRedis
        .mockResolvedValueOnce(null) // get → miss
        .mockResolvedValueOnce(undefined); // set → ok

      await childGuard.getBoundChildren("parent-001");

      // 再次请求应该走缓存或重新调 iam（inflight 已清理）
      mockSafeRedis
        .mockResolvedValueOnce(null) // get → miss
        .mockResolvedValueOnce(undefined); // set → ok

      await childGuard.getBoundChildren("parent-001");
      expect(iamClient.getChildrenCallCount).toBe(2);
    });
  });

  describe("缓存 JSON 解析失败降级", () => {
    it("Redis 返回非法 JSON 时降级到 iam 调用", async () => {
      mockSafeRedis
        .mockResolvedValueOnce("invalid-json") // get → 返回非法 JSON
        .mockResolvedValueOnce(undefined); // set → ok

      const children = await childGuard.getBoundChildren("parent-001");

      expect(children).toHaveLength(2);
      expect(iamClient.getChildrenCallCount).toBe(1);
    });
  });
});
