import { describe, expect, it } from "vitest";
import {
  buildOrgSelectionOptions,
  buildUsersMap,
  clampScale,
  filterSystemGlobalTenants,
  getMemberGridMetrics,
  getOrgNodeHeaderFill,
  getSemanticZoomMode,
  layoutForest,
  type OrgNode,
  resolveOrgChartFamilyRoot,
} from "./OrgChartPage";

function orgNode(id: string, children: OrgNode[] = [], level = 0): OrgNode {
  return {
    id,
    name: id,
    level,
    members: [],
    children,
    totalCount: 0,
    totalMemberIds: new Set<string>(),
    companyCode: id,
    type: level === 0 ? "COMPANY" : "USER_GROUP",
  };
}

function member(id: string) {
  return {
    id,
    email: `${id}@example.com`,
    name: id,
    role: "user",
    status: "active",
    companyCode: "root",
    grade: "사원",
    createdAt: "2026-05-11T00:00:00.000Z",
    updatedAt: "2026-05-11T00:00:00.000Z",
  };
}

function tenantNode(
  id: string,
  type: string,
  name: string,
  slug: string,
  children = [],
) {
  return {
    id,
    type,
    name,
    slug,
    children,
    description: "",
    status: "active",
    memberCount: 0,
    recursiveMemberCount: 0,
    createdAt: "2026-05-11T00:00:00.000Z",
    updatedAt: "2026-05-11T00:00:00.000Z",
  };
}

function getNodeBoundsAspectRatio(
  nodes: ReturnType<typeof layoutForest>["nodes"],
) {
  const minX = Math.min(...nodes.map((node) => node.x));
  const maxX = Math.max(...nodes.map((node) => node.x + node.width));
  const minY = Math.min(...nodes.map((node) => node.y));
  const maxY = Math.max(...nodes.map((node) => node.y + node.height));

  return (maxX - minX) / (maxY - minY);
}

describe("org chart layout", () => {
  it("keeps small sibling groups horizontal in automatic mode", () => {
    const children = Array.from({ length: 4 }, (_, index) =>
      orgNode(`child-${index + 1}`, [], 1),
    );
    const layout = layoutForest([orgNode("root", children)], new Set());
    const childNodes = layout.nodes.filter((node) =>
      node.node.id.startsWith("child-"),
    );

    expect(new Set(childNodes.map((node) => node.y)).size).toBe(1);
  });

  it("uses member columns in node bounds when the rendered node aspect ratio needs them", () => {
    const compactMembers = Array.from({ length: 10 }, (_, index) =>
      member(`member-${index + 1}`),
    );
    const node = {
      ...orgNode("root"),
      members: compactMembers,
      totalCount: compactMembers.length,
      totalMemberIds: new Set(compactMembers.map((item) => item.id)),
    };
    const layout = layoutForest([node], new Set());
    const rootNode = layout.nodes.find((item) => item.node.id === "root");

    expect(rootNode).toBeDefined();
    expect(rootNode?.width).toBeGreaterThan(240);
    expect(rootNode?.height).toBeLessThan(42 + 24 + 10 * 24);
    expect(layout.width).toBeGreaterThan((rootNode?.width ?? 0) + 72 * 2 - 1);
  });

  it("sizes member cards from an eight-character baseline and expands for long display names", () => {
    const shortMembers = Array.from({ length: 6 }, (_, index) => ({
      ...member(`short-${index + 1}`),
      name: `홍길${index + 1}`,
      grade: "책임",
    }));
    const longMembers = shortMembers.map((item, index) => ({
      ...item,
      id: `long-${index + 1}`,
      name: `매우긴사용자이름${index + 1}`,
    }));
    const shortLayout = layoutForest(
      [
        {
          ...orgNode("short"),
          members: shortMembers,
          totalCount: shortMembers.length,
          totalMemberIds: new Set(shortMembers.map((item) => item.id)),
        },
      ],
      new Set(),
    );
    const longLayout = layoutForest(
      [
        {
          ...orgNode("long"),
          members: longMembers,
          totalCount: longMembers.length,
          totalMemberIds: new Set(longMembers.map((item) => item.id)),
        },
      ],
      new Set(),
    );
    const shortNode = shortLayout.nodes.find(
      (item) => item.node.id === "short",
    );
    const longNode = longLayout.nodes.find((item) => item.node.id === "long");

    expect(shortNode?.width).toBeLessThan(320);
    expect(longNode?.width).toBeGreaterThan(shortNode?.width ?? 0);
  });

  it("uses compact member columns when another column improves the rendered ratio", () => {
    const tenMembers = Array.from({ length: 10 }, (_, index) =>
      member(`member-${index + 1}`),
    );
    const sixMembers = tenMembers.slice(0, 6);
    const sixLayout = layoutForest(
      [
        {
          ...orgNode("six"),
          members: sixMembers,
          totalCount: sixMembers.length,
          totalMemberIds: new Set(sixMembers.map((item) => item.id)),
        },
      ],
      new Set(),
    );
    const tenLayout = layoutForest(
      [
        {
          ...orgNode("ten"),
          members: tenMembers,
          totalCount: tenMembers.length,
          totalMemberIds: new Set(tenMembers.map((item) => item.id)),
        },
      ],
      new Set(),
    );
    const sixNode = sixLayout.nodes.find((item) => item.node.id === "six");
    const tenNode = tenLayout.nodes.find((item) => item.node.id === "ten");

    expect(sixNode?.width).toBeGreaterThan(240);
    expect(tenNode?.width).toBe(sixNode?.width);
    expect(sixNode?.height).toBeLessThan(42 + 24 + 6 * 24);
    expect(tenNode?.height).toBeLessThan(42 + 24 + 10 * 24);
  });

  it("chooses member columns from the rendered node aspect ratio instead of fixed five-member buckets", () => {
    expect(getMemberGridMetrics(6)).toEqual({ columnCount: 2, rowCount: 3 });
    expect(getMemberGridMetrics(10)).toEqual({ columnCount: 2, rowCount: 5 });
    expect(getMemberGridMetrics(25)).toEqual({ columnCount: 4, rowCount: 7 });
  });

  it("sorts members by normalized rank inside the same organization", () => {
    const members = [
      { ...member("staff"), name: "사원", grade: "사원" },
      { ...member("principal"), name: "수석", grade: "수석연구원" },
      { ...member("director"), name: "전무", grade: "전무이사" },
      { ...member("lead"), name: "책임", grade: "책임" },
    ];
    const layout = layoutForest(
      [
        {
          ...orgNode("root"),
          members,
          totalCount: members.length,
          totalMemberIds: new Set(members.map((item) => item.id)),
        },
      ],
      new Set(),
    );
    const rootNode = layout.nodes.find((item) => item.node.id === "root");

    expect(rootNode?.members.map((item) => item.id)).toEqual([
      "director",
      "principal",
      "lead",
      "staff",
    ]);
  });

  it("uses multi-column layout by default when sibling width crosses the threshold", () => {
    const children = Array.from({ length: 13 }, (_, index) =>
      orgNode(`child-${index + 1}`, [], 1),
    );
    const layout = layoutForest([orgNode("root", children)], new Set());
    const childNodes = layout.nodes.filter((node) =>
      node.node.id.startsWith("child-"),
    );
    const uniqueChildRows = new Set(childNodes.map((node) => node.y));
    const childSpan =
      Math.max(...childNodes.map((node) => node.x + node.width)) -
      Math.min(...childNodes.map((node) => node.x));
    const aspectRatio = getNodeBoundsAspectRatio(layout.nodes);

    expect(childNodes).toHaveLength(13);
    expect(uniqueChildRows.size).toBeGreaterThan(1);
    expect(aspectRatio).toBeGreaterThanOrEqual(1.41);
    expect(aspectRatio).toBeLessThanOrEqual(1.61);
    expect(childSpan).toBeLessThan(13 * 240 + 12 * 80);
    expect(
      layout.edges.filter((edge) => edge.key.startsWith("root->")),
    ).toHaveLength(13);
    expect(
      layout.edges.filter(
        (edge) => edge.key.startsWith("root->") && edge.visibleByDefault,
      ),
    ).toHaveLength(new Set(childNodes.map((node) => node.x)).size);
  });

  it("tunes column and row gaps after column selection to keep auto layout near the target aspect ratio", () => {
    const children = Array.from({ length: 5 }, (_, index) =>
      orgNode(`child-${index + 1}`, [], 1),
    );
    const layout = layoutForest([orgNode("root", children)], new Set());
    const childNodes = layout.nodes.filter((node) =>
      node.node.id.startsWith("child-"),
    );
    const aspectRatio = getNodeBoundsAspectRatio(layout.nodes);

    expect(new Set(childNodes.map((node) => node.x)).size).toBe(4);
    expect(aspectRatio).toBeGreaterThanOrEqual(1.41);
    expect(aspectRatio).toBeLessThanOrEqual(1.61);
  });

  it("keeps direct siblings on one level in top-down mode", () => {
    const children = Array.from({ length: 13 }, (_, index) =>
      orgNode(`child-${index + 1}`, [], 1),
    );
    const layout = layoutForest([orgNode("root", children)], new Set(), {
      childLayoutMode: "topDown",
    });
    const childNodes = layout.nodes.filter((node) =>
      node.node.id.startsWith("child-"),
    );
    const uniqueChildRows = new Set(childNodes.map((node) => node.y));

    expect(childNodes).toHaveLength(13);
    expect(uniqueChildRows.size).toBe(1);
  });

  it("places children in three fixed columns with centered parent edges", () => {
    const children = Array.from({ length: 10 }, (_, index) =>
      orgNode(`child-${index + 1}`, [], 1),
    );
    const layout = layoutForest([orgNode("root", children)], new Set(), {
      childLayoutMode: "threeColumn",
    });
    const childNodes = layout.nodes.filter((node) =>
      node.node.id.startsWith("child-"),
    );
    const uniqueChildColumns = new Set(childNodes.map((node) => node.x));
    const uniqueChildRows = new Set(childNodes.map((node) => node.y));
    const rootEdges = layout.edges.filter((edge) =>
      edge.key.startsWith("root->"),
    );

    expect(uniqueChildColumns.size).toBe(3);
    expect(uniqueChildRows.size).toBe(4);
    expect(rootEdges).toHaveLength(10);
    expect(rootEdges.filter((edge) => edge.visibleByDefault)).toHaveLength(3);
  });

  it("places the deepest child subtree in the first multi-column section", () => {
    const children = [
      orgNode("shallow-1", [], 1),
      orgNode("shallow-2", [], 1),
      orgNode("shallow-3", [], 1),
      orgNode(
        "deep",
        [
          orgNode(
            "deep-branch",
            [orgNode("deep-leaf", [orgNode("deep-tail", [], 4)], 3)],
            2,
          ),
        ],
        1,
      ),
      orgNode("shallow-4", [], 1),
      orgNode("shallow-5", [], 1),
    ];
    const layout = layoutForest([orgNode("root", children)], new Set(), {
      childLayoutMode: "threeColumn",
    });
    const rootEdges = layout.edges.filter((edge) =>
      edge.key.startsWith("root->"),
    );

    expect(rootEdges.map((edge) => edge.key)).toContain("root->deep");
  });

  it("centers a parent over the full child span in multi-column mode", () => {
    const children = [
      orgNode(
        "deep",
        [
          orgNode(
            "deep-branch",
            [orgNode("deep-leaf", [orgNode("deep-tail", [], 4)], 3)],
            2,
          ),
        ],
        1,
      ),
      ...Array.from({ length: 9 }, (_, index) =>
        orgNode(`shallow-${index + 1}`, [], 1),
      ),
    ];
    const layout = layoutForest([orgNode("root", children)], new Set(), {
      childLayoutMode: "threeColumn",
    });
    const rootNode = layout.nodes.find((node) => node.node.id === "root");
    const directChildren = layout.nodes.filter((node) => node.node.level === 1);
    const childSpanCenter =
      (Math.min(...directChildren.map((node) => node.x + node.width / 2)) +
        Math.max(...directChildren.map((node) => node.x + node.width / 2))) /
      2;
    const rootCenter = rootNode ? rootNode.x + rootNode.width / 2 : 0;

    expect(rootNode).toBeDefined();
    expect(rootCenter).toBeCloseTo(childSpanCenter, 5);
  });

  it("centers parents above the tidy child span", () => {
    const children = [
      orgNode("left", [orgNode("left-a", [], 2), orgNode("left-b", [], 2)], 1),
      orgNode("middle", [], 1),
      orgNode(
        "right",
        [orgNode("right-a", [], 2), orgNode("right-b", [], 2)],
        1,
      ),
    ];
    const layout = layoutForest([orgNode("root", children)], new Set(), {
      childLayoutMode: "topDown",
    });
    const rootNode = layout.nodes.find((node) => node.node.id === "root");
    const directChildren = layout.nodes.filter((node) =>
      ["left", "middle", "right"].includes(node.node.id),
    );
    const childSpanCenter =
      (Math.min(...directChildren.map((node) => node.x + node.width / 2)) +
        Math.max(...directChildren.map((node) => node.x + node.width / 2))) /
      2;
    const rootCenter = rootNode ? rootNode.x + rootNode.width / 2 : 0;

    expect(rootNode).toBeDefined();
    expect(rootCenter).toBeCloseTo(childSpanCenter, 5);
  });

  it("keeps compressed subtrees from overlapping on shared vertical bands", () => {
    const layout = layoutForest(
      [
        orgNode("root", [
          orgNode(
            "left",
            [orgNode("left-a", [], 2), orgNode("left-b", [], 2)],
            1,
          ),
          orgNode(
            "right",
            [orgNode("right-a", [], 2), orgNode("right-b", [], 2)],
            1,
          ),
        ]),
      ],
      new Set(),
    );

    for (const node of layout.nodes) {
      for (const other of layout.nodes) {
        if (node.node.id >= other.node.id) continue;
        const verticalOverlap =
          node.y < other.y + other.height && other.y < node.y + node.height;
        const horizontalOverlap =
          node.x < other.x + other.width && other.x < node.x + node.width;

        expect(
          verticalOverlap && horizontalOverlap,
          `${node.node.id} overlaps ${other.node.id}`,
        ).toBe(false);
      }
    }
  });

  it("keeps zoom limits wide enough for large SVG organization charts", () => {
    expect(clampScale(0.08)).toBe(0.08);
    expect(clampScale(32)).toBe(32);
    expect(clampScale(64)).toBe(32);
  });

  it("switches semantic zoom modes from overview to detail", () => {
    expect(getSemanticZoomMode(0.12)).toBe("overview");
    expect(getSemanticZoomMode(0.4)).toBe("compact");
    expect(getSemanticZoomMode(0.8)).toBe("detail");
  });

  it("uses distinct header fills by organization depth", () => {
    expect(getOrgNodeHeaderFill(0, "family")).toBe("#000000");
    expect(getOrgNodeHeaderFill(0, "saman")).toBe("#f58220");
    expect(getOrgNodeHeaderFill(0, "hanmac")).toBe("#1e489d");
    expect(getOrgNodeHeaderFill(0, "gpdtdc")).toBe("#4b746d");
    expect(getOrgNodeHeaderFill(0, "baron")).toBe("#004cbf");
    expect(getOrgNodeHeaderFill(1, "saman")).not.toBe(
      getOrgNodeHeaderFill(0, "saman"),
    );
    expect(getOrgNodeHeaderFill(2, "saman")).not.toBe(
      getOrgNodeHeaderFill(1, "saman"),
    );
  });

  it("orders top organization choices by the hanmac family policy", () => {
    const familyRoot = tenantNode(
      "family",
      "COMPANY_GROUP",
      "한맥가족",
      "hanmac-family",
      [
        tenantNode("saman", "COMPANY", "삼안", "saman"),
        tenantNode("baron", "COMPANY_GROUP", "바론그룹", "baron-group"),
        tenantNode("hanmac", "COMPANY", "한맥기술", "hanmac"),
        tenantNode("gpdtdc", "ORGANIZATION", "총괄기획&기술개발센터", "gpdtdc"),
      ],
    );

    expect(
      buildOrgSelectionOptions(familyRoot).map((option) => option.label),
    ).toEqual(["총괄기획&기술개발센터", "삼안", "한맥기술", "바론그룹"]);
  });

  it("selects hanmac family as the default root even when public sector group is listed first", () => {
    const publicSector = tenantNode(
      "public-sector",
      "COMPANY_GROUP",
      "공공기관",
      "public-sector",
    );
    const familyRoot = tenantNode(
      "family",
      "COMPANY_GROUP",
      "한맥가족",
      "hanmac-family",
      [tenantNode("saman", "COMPANY", "삼안", "saman")],
    );

    expect(resolveOrgChartFamilyRoot([publicSector, familyRoot])?.id).toBe(
      "family",
    );
  });

  it("hides internal organizations by default and includes them for internal mode", () => {
    const visibleParent = tenantNode(
      "visible-parent",
      "COMPANY",
      "공개 회사",
      "visible-parent",
    );
    const internalOrg = {
      ...tenantNode(
        "internal-org",
        "ORGANIZATION",
        "내부 조직",
        "internal-org",
      ),
      parentId: "visible-parent",
      config: { visibility: "internal" },
    };
    const internalChild = {
      ...tenantNode(
        "internal-child",
        "ORGANIZATION",
        "내부 하위",
        "internal-child",
      ),
      parentId: "internal-org",
    };
    const privateOrg = {
      ...tenantNode(
        "private-org",
        "ORGANIZATION",
        "비공개 조직",
        "private-org",
      ),
      parentId: "visible-parent",
      config: { visibility: "private" },
    };
    const publicOrg = {
      ...tenantNode("public-org", "ORGANIZATION", "공개 조직", "public-org"),
      parentId: "visible-parent",
    };

    const tenants = [
      visibleParent,
      internalOrg,
      internalChild,
      privateOrg,
      publicOrg,
    ];

    expect(
      filterSystemGlobalTenants(tenants, "public").map((tenant) => tenant.id),
    ).toEqual(["visible-parent", "public-org"]);
    expect(
      filterSystemGlobalTenants(tenants, "internal").map((tenant) => tenant.id),
    ).toEqual([
      "visible-parent",
      "internal-org",
      "internal-child",
      "public-org",
    ]);
  });

  it("maps legacy companyCode users to matching tenant slugs", () => {
    const usersMap = buildUsersMap(
      [
        {
          ...member("engineering-user"),
          companyCode: "engineering",
          tenantSlug: undefined,
          tenant: undefined,
          joinedTenants: undefined,
        },
      ],
      [tenantNode("engineering", "ORGANIZATION", "Engineering", "engineering")],
      { activeOnly: true },
    );

    expect(usersMap.get("engineering")?.map((user) => user.id)).toEqual([
      "engineering-user",
    ]);
  });

  it("maps GPDTDC representative users to their visible leaf appointment", () => {
    const gpdtdc = tenantNode("gpdtdc", "COMPANY", "GPDTDC", "gpdtdc");
    const tdc = {
      ...tenantNode("tdc", "ORGANIZATION", "기술개발센터", "tdc"),
      parentId: "gpdtdc",
    };
    const leaf = {
      ...tenantNode("tdc-leaf", "USER_GROUP", "기술개발센터 1팀", "tdc-leaf"),
      parentId: "tdc",
    };
    const rootNode = {
      ...gpdtdc,
      children: [{ ...tdc, children: [leaf] }],
    };

    const usersMap = buildUsersMap(
      [
        {
          ...member("gpdtdc-user"),
          companyCode: undefined,
          tenantSlug: "gpdtdc",
          metadata: {
            additionalAppointments: [
              {
                tenantSlug: "internal-planning",
                isPrimary: true,
              },
              {
                tenantSlug: "tdc-leaf",
                isPrimary: false,
                grade: "책임",
                position: "팀장",
              },
            ],
          },
          joinedTenants: undefined,
        },
      ],
      [rootNode],
      { activeOnly: true },
    );

    expect(usersMap.get("gpdtdc")).toBeUndefined();
    expect(usersMap.get("internal-planning")).toBeUndefined();
    expect(usersMap.get("tdc-leaf")?.map((user) => user.id)).toEqual([
      "gpdtdc-user",
    ]);
  });

  it("does not fall back to a visible parent for hidden leaf memberships", () => {
    const gpdtdc = tenantNode("gpdtdc", "COMPANY", "GPDTDC", "gpdtdc");
    const internalLeaf = {
      ...tenantNode(
        "internal-leaf",
        "USER_GROUP",
        "내부 구성 조직",
        "internal-leaf",
      ),
      parentId: "gpdtdc",
    };

    const usersMap = buildUsersMap(
      [
        {
          ...member("hidden-only-user"),
          companyCode: undefined,
          tenantSlug: "gpdtdc",
          metadata: {
            additionalAppointments: [
              {
                tenantSlug: "internal-leaf",
                isPrimary: true,
              },
            ],
          },
          joinedTenants: undefined,
        },
      ],
      [gpdtdc],
      {
        activeOnly: true,
        membershipRootNodes: [{ ...gpdtdc, children: [internalLeaf] }],
      },
    );

    expect(usersMap.get("gpdtdc")).toBeUndefined();
    expect(usersMap.get("internal-leaf")).toBeUndefined();
  });
});
