/**
 * Homework Resolver 单元测试.
 *
 * 测试覆盖:
 *   - myHomework Query: 缓存命中 / 缓存未命中 / 下游失败
 *   - submitHomework Mutation: 正常提交 / Zod 校验失败 / 越权防御 / 下游失败 / 缓存失效
 */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { homeworkResolvers } from "./homework.resolver.js";
import type { StudentBffContext } from "../../shared/graphql/yoga.js";
import {
  UnauthorizedError,
  ValidationError,
  ForbiddenResourceError,
} from "../../shared/errors/application-error.js";

// Mock env to disable DEV_MODE
vi.mock("../../config/env.js", () => ({
  env: { DEV_MODE: false, NODE_ENV: "test" },
}));

function mockContext(
  overrides: Partial<StudentBffContext> = {},
): StudentBffContext {
  const downstream = {
    call: vi.fn(),
    callStream: vi.fn(),
    callAll: vi.fn(),
  };
  const redis = {};
  const cache = {
    get: vi.fn().mockResolvedValue(null),
    set: vi.fn().mockResolvedValue(undefined),
    invalidate: vi.fn().mockResolvedValue(undefined),
    invalidateByPrefix: vi.fn().mockResolvedValue(undefined),
    ping: vi.fn().mockResolvedValue(true),
  };
  const dataLoaders = {} as never;
  return {
    userId: "u-stu-001",
    traceId: "trace-test-001",
    userRoles: ["student"],
    downstream: downstream as never,
    redis: redis as never,
    cache: cache as never,
    dataLoaders,
    requestId: "trace-test-001",
    ...overrides,
  };
}

describe("homeworkResolvers", () => {
  let ctx: StudentBffContext;

  beforeEach(() => {
    ctx = mockContext();
  });

  describe("Query.myHomework", () => {
    it("should throw UnauthorizedError when userId is null", async () => {
      ctx.userId = null;
      await expect(
        homeworkResolvers.Query.myHomework(null, {}, ctx),
      ).rejects.toThrow(UnauthorizedError);
    });

    it("should return cached data when cache hits", async () => {
      const cachedData = { homework: [{ id: "hw-001" }] };
      ctx.cache.get = vi.fn().mockResolvedValue(cachedData);

      const result = (await homeworkResolvers.Query.myHomework(
        null,
        {},
        ctx,
      )) as { success: boolean; data: unknown };
      expect(result.success).toBe(true);
      expect(result.data).toEqual(cachedData);
      expect(ctx.downstream.call).not.toHaveBeenCalled();
    });

    it("should call downstream when cache misses", async () => {
      const downstreamData = { homework: [{ id: "hw-001", title: "Math HW" }] };
      ctx.downstream.call = vi.fn().mockResolvedValue(downstreamData);

      const result = (await homeworkResolvers.Query.myHomework(
        null,
        { status: "ASSIGNED", classId: "c-001" },
        ctx,
      )) as { success: boolean };
      expect(result.success).toBe(true);
      expect(ctx.downstream.call).toHaveBeenCalledWith(
        "core-edu",
        "ListHomeworkByStudent",
        { studentId: "u-stu-001", status: "ASSIGNED", classId: "c-001" },
        expect.objectContaining({
          traceId: "trace-test-001",
          metadata: { "x-user-id": "u-stu-001" },
        }),
      );
      expect(ctx.cache.set).toHaveBeenCalled();
    });

    it("should return fail response when downstream fails", async () => {
      ctx.downstream.call = vi
        .fn()
        .mockRejectedValue(new Error("gRPC unavailable"));

      const result = (await homeworkResolvers.Query.myHomework(
        null,
        {},
        ctx,
      )) as { success: boolean; error: { code: string } };
      expect(result.success).toBe(false);
      expect(result.error.code).toBe("BFF_STUDENT_BAD_GATEWAY");
    });
  });

  describe("Mutation.submitHomework", () => {
    const validInput = {
      homeworkId: "hw-001",
      studentId: "u-stu-001",
      answers: [{ questionId: "q-001", content: "My answer" }],
    };

    it("should throw UnauthorizedError when userId is null", async () => {
      ctx.userId = null;
      await expect(
        homeworkResolvers.Mutation.submitHomework(
          null,
          { input: validInput },
          ctx,
        ),
      ).rejects.toThrow(UnauthorizedError);
    });

    it("should throw ValidationError when input is invalid", async () => {
      await expect(
        homeworkResolvers.Mutation.submitHomework(
          null,
          { input: { homeworkId: "", answers: [] } },
          ctx,
        ),
      ).rejects.toThrow(ValidationError);
    });

    it("should throw ForbiddenResourceError when studentId does not match userId (B4)", async () => {
      const maliciousInput = {
        ...validInput,
        studentId: "u-stu-002",
      };
      await expect(
        homeworkResolvers.Mutation.submitHomework(
          null,
          { input: maliciousInput },
          ctx,
        ),
      ).rejects.toThrow(ForbiddenResourceError);
    });

    it("should submit successfully when studentId matches userId", async () => {
      const submitResult = {
        submissionId: "sub-001",
        homeworkId: "hw-001",
        submittedAt: "2026-07-10T10:00:00Z",
        status: "SUBMITTED",
      };
      ctx.downstream.call = vi.fn().mockResolvedValue(submitResult);

      const result = (await homeworkResolvers.Mutation.submitHomework(
        null,
        { input: validInput },
        ctx,
      )) as { success: boolean };

      expect(result.success).toBe(true);
      expect(ctx.downstream.call).toHaveBeenCalledWith(
        "core-edu",
        "SubmitHomework",
        {
          homeworkId: "hw-001",
          studentId: "u-stu-001",
          answers: validInput.answers,
        },
        expect.objectContaining({
          metadata: { "x-user-id": "u-stu-001" },
        }),
      );
      expect(ctx.cache.invalidate).toHaveBeenCalledWith(
        "homework",
        "u-stu-001",
      );
      expect(ctx.cache.invalidate).toHaveBeenCalledWith(
        "dashboard",
        "u-stu-001",
      );
    });

    it("should submit successfully when studentId is not provided in input", async () => {
      const inputWithoutStudentId = {
        homeworkId: "hw-001",
        answers: [{ questionId: "q-001", content: "Answer" }],
      };
      ctx.downstream.call = vi.fn().mockResolvedValue({
        submissionId: "sub-002",
        homeworkId: "hw-001",
        submittedAt: "2026-07-10T10:00:00Z",
        status: "SUBMITTED",
      });

      const result = (await homeworkResolvers.Mutation.submitHomework(
        null,
        { input: inputWithoutStudentId },
        ctx,
      )) as { success: boolean };

      expect(result.success).toBe(true);
    });

    it("should return fail response when downstream fails", async () => {
      ctx.downstream.call = vi
        .fn()
        .mockRejectedValue(new Error("Submission failed"));

      const result = (await homeworkResolvers.Mutation.submitHomework(
        null,
        { input: validInput },
        ctx,
      )) as { success: boolean; error: { code: string } };

      expect(result.success).toBe(false);
      expect(result.error.code).toBe("BFF_STUDENT_BAD_GATEWAY");
    });

    it("should validate answer content max length", async () => {
      const longContentInput = {
        homeworkId: "hw-001",
        studentId: "u-stu-001",
        answers: [{ questionId: "q-001", content: "x".repeat(10001) }],
      };
      await expect(
        homeworkResolvers.Mutation.submitHomework(
          null,
          { input: longContentInput },
          ctx,
        ),
      ).rejects.toThrow(ValidationError);
    });

    it("should validate attachments are valid URLs", async () => {
      const invalidAttachmentInput = {
        homeworkId: "hw-001",
        studentId: "u-stu-001",
        answers: [
          {
            questionId: "q-001",
            content: "Answer",
            attachments: ["not-a-url"],
          },
        ],
      };
      await expect(
        homeworkResolvers.Mutation.submitHomework(
          null,
          { input: invalidAttachmentInput },
          ctx,
        ),
      ).rejects.toThrow(ValidationError);
    });
  });
});
