/**
 * AuthorizationGuard 单元测试 - B4 自我越权防御.
 *
 * 测试覆盖:
 *   - extractUserIdFromRequest: x-user-id 头提取
 *   - extractTraceIdFromRequest: x-request-id 头提取
 *   - extractUserRolesFromRequest: x-user-roles 头提取
 *   - assertOwnData: 场景 A (资源无归属)
 *   - assertIdentityMatch: 场景 B (身份不一致)
 *   - DEV_MODE 放行
 *   - AuthorizationGuard.canActivate
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { Request } from "express";

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

// Re-import after mock
const { env } = await import("../../config/env.js");
const {
  AuthorizationGuard,
  extractUserIdFromRequest,
  extractTraceIdFromRequest,
  extractUserRolesFromRequest,
  assertOwnData,
  assertIdentityMatch,
} = await import("./authorization.guard.js");
const { ForbiddenResourceError, IdentityMismatchError, UnauthorizedError } = await import(
  "../../shared/errors/application-error.js"
);

function mockRequest(headers: Record<string, string | undefined> = {}): Request {
  return { headers } as unknown as Request;
}

describe("AuthorizationGuard", () => {
  describe("extractUserIdFromRequest", () => {
    it("should extract x-user-id header", () => {
      const req = mockRequest({ "x-user-id": "u-stu-001" });
      expect(extractUserIdFromRequest(req)).toBe("u-stu-001");
    });

    it("should return null when header is missing", () => {
      const req = mockRequest({});
      expect(extractUserIdFromRequest(req)).toBeNull();
    });

    it("should return null when header is empty string", () => {
      const req = mockRequest({ "x-user-id": "" });
      expect(extractUserIdFromRequest(req)).toBeNull();
    });
  });

  describe("extractTraceIdFromRequest", () => {
    it("should extract x-request-id header", () => {
      const req = mockRequest({ "x-request-id": "trace-abc" });
      expect(extractTraceIdFromRequest(req)).toBe("trace-abc");
    });

    it("should return 'unknown' when header is missing", () => {
      const req = mockRequest({});
      expect(extractTraceIdFromRequest(req)).toBe("unknown");
    });

    it("should return 'unknown' when header is empty", () => {
      const req = mockRequest({ "x-request-id": "" });
      expect(extractTraceIdFromRequest(req)).toBe("unknown");
    });
  });

  describe("extractUserRolesFromRequest", () => {
    it("should extract comma-separated roles", () => {
      const req = mockRequest({ "x-user-roles": "student,monitor" });
      expect(extractUserRolesFromRequest(req)).toEqual(["student", "monitor"]);
    });

    it("should trim whitespace", () => {
      const req = mockRequest({ "x-user-roles": " student , monitor " });
      expect(extractUserRolesFromRequest(req)).toEqual(["student", "monitor"]);
    });

    it("should return empty array when header is missing", () => {
      const req = mockRequest({});
      expect(extractUserRolesFromRequest(req)).toEqual([]);
    });

    it("should filter out empty entries", () => {
      const req = mockRequest({ "x-user-roles": "student,,monitor," });
      expect(extractUserRolesFromRequest(req)).toEqual(["student", "monitor"]);
    });
  });

  describe("assertOwnData (场景 A)", () => {
    beforeEach(() => {
      env.DEV_MODE = false;
    });

    it("should pass when requestedStudentId matches userId", () => {
      expect(() => assertOwnData("u-001", "u-001")).not.toThrow();
    });

    it("should pass when requestedStudentId is undefined", () => {
      expect(() => assertOwnData("u-001", undefined)).not.toThrow();
    });

    it("should pass when requestedStudentId is null", () => {
      expect(() => assertOwnData("u-001", null)).not.toThrow();
    });

    it("should throw ForbiddenResourceError when studentId differs", () => {
      expect(() => assertOwnData("u-001", "u-002")).toThrow(ForbiddenResourceError);
    });

    it("should not throw when DEV_MODE is true", () => {
      env.DEV_MODE = true;
      expect(() => assertOwnData("u-001", "u-002")).not.toThrow();
      env.DEV_MODE = false;
    });
  });

  describe("assertIdentityMatch (场景 B)", () => {
    beforeEach(() => {
      env.DEV_MODE = false;
    });

    it("should pass when bodyUserId matches userId", () => {
      expect(() => assertIdentityMatch("u-001", "u-001")).not.toThrow();
    });

    it("should pass when bodyUserId is undefined", () => {
      expect(() => assertIdentityMatch("u-001", undefined)).not.toThrow();
    });

    it("should pass when bodyUserId is null", () => {
      expect(() => assertIdentityMatch("u-001", null)).not.toThrow();
    });

    it("should throw IdentityMismatchError when bodyUserId differs", () => {
      expect(() => assertIdentityMatch("u-001", "u-002")).toThrow(IdentityMismatchError);
    });

    it("should not throw when DEV_MODE is true", () => {
      env.DEV_MODE = true;
      expect(() => assertIdentityMatch("u-001", "u-002")).not.toThrow();
      env.DEV_MODE = false;
    });
  });

  describe("AuthorizationGuard.canActivate", () => {
    let guard: InstanceType<typeof AuthorizationGuard>;

    beforeEach(() => {
      guard = new AuthorizationGuard();
      env.DEV_MODE = false;
    });

    afterEach(() => {
      env.DEV_MODE = false;
    });

    it("should return true when DEV_MODE is true", () => {
      env.DEV_MODE = true;
      const ctx = {
        switchToHttp: () => ({ getRequest: () => mockRequest({}) }),
      };
      expect(guard.canActivate(ctx as never)).toBe(true);
    });

    it("should return true when x-user-id is present", () => {
      const ctx = {
        switchToHttp: () => ({ getRequest: () => mockRequest({ "x-user-id": "u-001" }) }),
      };
      expect(guard.canActivate(ctx as never)).toBe(true);
    });

    it("should throw UnauthorizedError when x-user-id is missing", () => {
      const ctx = {
        switchToHttp: () => ({ getRequest: () => mockRequest({}) }),
      };
      expect(() => guard.canActivate(ctx as never)).toThrow(UnauthorizedError);
    });

    it("should throw UnauthorizedError when x-user-id is empty", () => {
      const ctx = {
        switchToHttp: () => ({ getRequest: () => mockRequest({ "x-user-id": "" }) }),
      };
      expect(() => guard.canActivate(ctx as never)).toThrow(UnauthorizedError);
    });
  });
});
