import { describe, it, expect, beforeEach, vi } from "vitest";

// 使用 vi.hoisted 确保 mockDelCalls 在 mock factory 之前定义
const mockDelCalls = vi.hoisted(() => [] as string[]);

// Mock safeRedis：捕获 del 调用以验证失效的缓存 key
vi.mock("../../src/shared/cache/redis.client.js", () => ({
  safeRedis: vi.fn(
    async (
      fn: (client: unknown) => Promise<unknown>,
      fallback: unknown,
    ): Promise<unknown> => {
      const mockClient = {
        del: (key: string): number => {
          mockDelCalls.push(key);
          return 1;
        },
        get: (): null => null,
        set: (): string => "OK",
        ping: (): string => "PONG",
      };
      try {
        return await fn(mockClient);
      } catch {
        return fallback;
      }
    },
  ),
  getRedisClient: vi.fn(() => null),
  closeRedisClient: vi.fn(),
}));

import { CacheInvalidationHandler } from "../../src/shared/kafka/handlers/cache-invalidation.handler.js";
import type {
  EventHandler,
  EventContext,
} from "../../src/shared/kafka/handlers/event-handler.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";

const mockSafeRedis = vi.mocked(safeRedis);

interface TeachingEventBody {
  studentId?: string;
  childId?: string;
  classId?: string;
  parentId?: string;
}

describe("测试用例 10：Kafka 缓存失效（Integration）", () => {
  let handler: EventHandler<TeachingEventBody>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockDelCalls.length = 0;
    handler = new CacheInvalidationHandler();
  });

  describe("edu.teaching.grade.recorded", () => {
    it("失效 grades:{childId}:* + dashboard:{parentId}", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.grade.recorded",
        eventId: "evt-001",
        key: "parent-001",
        body: {
          childId: "student-001",
          parentId: "parent-001",
          classId: "class-001",
        },
      };

      await handler.handle(ctx);

      // 失效 grades:student-001:1 + dashboard:parent-001
      expect(mockDelCalls).toContain("grades:student-001:1");
      expect(mockDelCalls).toContain("dashboard:parent-001");
      expect(mockDelCalls).toHaveLength(2);
    });

    it("使用 studentId 作为 childId fallback", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.grade.recorded",
        eventId: "evt-002",
        key: "parent-001",
        body: {
          studentId: "student-002",
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      // childId 从 studentId fallback
      expect(mockDelCalls).toContain("grades:student-002:1");
      expect(mockDelCalls).toContain("dashboard:parent-001");
    });

    it("无 parentId 时只失效 grades 缓存", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.grade.recorded",
        eventId: "evt-003",
        key: "student-001",
        body: {
          childId: "student-001",
        },
      };

      await handler.handle(ctx);

      expect(mockDelCalls).toContain("grades:student-001:1");
      expect(mockDelCalls).not.toContain("dashboard:parent-001");
      expect(mockDelCalls).toHaveLength(1);
    });
  });

  describe("edu.teaching.homework.graded", () => {
    it("失效 homework:{childId}:{classId} + dashboard:{parentId}", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.homework.graded",
        eventId: "evt-004",
        key: "parent-001",
        body: {
          childId: "student-001",
          classId: "class-001",
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      expect(mockDelCalls).toContain("homework:student-001:class-001");
      expect(mockDelCalls).toContain("dashboard:parent-001");
      expect(mockDelCalls).toHaveLength(2);
    });

    it("无 classId 时只失效 dashboard", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.homework.graded",
        eventId: "evt-005",
        key: "parent-001",
        body: {
          childId: "student-001",
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      // 无 classId 时 homework 缓存不失效（条件：childId && classId）
      expect(mockDelCalls).not.toContain("homework:student-001:undefined");
      expect(mockDelCalls).toContain("dashboard:parent-001");
      expect(mockDelCalls).toHaveLength(1);
    });
  });

  describe("edu.teaching.exam.published", () => {
    it("失效 exams:{childId}:{classId} + dashboard:{parentId}", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.exam.published",
        eventId: "evt-006",
        key: "parent-001",
        body: {
          childId: "student-001",
          classId: "class-001",
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      expect(mockDelCalls).toContain("exams:student-001:class-001");
      expect(mockDelCalls).toContain("dashboard:parent-001");
      expect(mockDelCalls).toHaveLength(2);
    });
  });

  describe("edu.notification.read", () => {
    it("失效 notifications:{parentId}:1", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.notification.read",
        eventId: "evt-007",
        key: "parent-001",
        body: {
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      expect(mockDelCalls).toContain("notifications:parent-001:1");
      expect(mockDelCalls).toHaveLength(1);
    });
  });

  describe("edu.notification.recalled", () => {
    it("失效 notifications:{parentId}:1", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.notification.recalled",
        eventId: "evt-008",
        key: "parent-001",
        body: {
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      expect(mockDelCalls).toContain("notifications:parent-001:1");
      expect(mockDelCalls).toHaveLength(1);
    });
  });

  describe("未知 topic", () => {
    it("不失效任何缓存", async () => {
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.unknown.event",
        eventId: "evt-009",
        key: "parent-001",
        body: {
          childId: "student-001",
          parentId: "parent-001",
        },
      };

      await handler.handle(ctx);

      expect(mockDelCalls).toHaveLength(0);
    });
  });

  describe("P5 场景：成绩录入后缓存失效全链路", () => {
    it("成绩事件同时失效 grades + dashboard（前端刷新后重新拉取）", async () => {
      // 模拟真实场景：教师录入成绩 → Kafka → 缓存失效 → 前端下次查询走下游
      const ctx: EventContext<TeachingEventBody> = {
        topic: "edu.teaching.grade.recorded",
        eventId: "evt-full-chain",
        key: "parent-001",
        body: {
          childId: "student-001",
          parentId: "parent-001",
          classId: "class-001",
        },
      };

      await handler.handle(ctx);

      // 验证 safeRedis 被调用了 2 次（grades del + dashboard del）
      expect(mockSafeRedis).toHaveBeenCalledTimes(2);

      // 验证失效的 key 正确
      expect(mockDelCalls).toContain("grades:student-001:1");
      expect(mockDelCalls).toContain("dashboard:parent-001");
    });
  });
});
