import { describe, expect, it } from "vitest";
import {
  resolveEditingAffordances,
  resolveEditingSections,
  type EditableElementFacts,
} from "./affordances";

function baseFacts(over: Partial<EditableElementFacts> = {}): EditableElementFacts {
  return {
    hasStableTarget: true,
    tag: "div",
    inlineStyles: {},
    computedStyles: {},
    isCompositionHost: false,
    isCompositionRoot: false,
    isInsideLockedComposition: false,
    isMasterView: false,
    existsInSource: true,
    hasEditableText: false,
    hasTimingStart: false,
    animationCount: 0,
    ...over,
  };
}

describe("resolveEditingAffordances — capabilities", () => {
  it("locked composition: nothing editable, not selectable, reason set", () => {
    const a = resolveEditingAffordances(baseFacts({ isInsideLockedComposition: true }));
    expect(a.capabilities).toMatchObject({
      canSelect: false,
      canEditStyles: false,
      canMove: false,
    });
    expect(a.capabilities.reasonIfDisabled).toContain("locked composition");
  });

  it("no stable target: not editable but selectable", () => {
    const a = resolveEditingAffordances(baseFacts({ hasStableTarget: false }));
    expect(a.capabilities.canSelect).toBe(true);
    expect(a.capabilities.canEditStyles).toBe(false);
  });

  it("not in source: script-generated, select-only", () => {
    const a = resolveEditingAffordances(baseFacts({ existsInSource: false }));
    expect(a.capabilities.canSelect).toBe(true);
    expect(a.capabilities.canEditStyles).toBe(false);
    expect(a.capabilities.reasonIfDisabled).toContain("generated by a script");
  });

  it("composition root: edit styles only, no move/resize, and NOT croppable (it is the canvas)", () => {
    const a = resolveEditingAffordances(baseFacts({ isCompositionRoot: true }));
    expect(a.capabilities).toMatchObject({ canEditStyles: true, canMove: false, canResize: false });
    expect(a.capabilities.canCrop).toBe(false);
  });

  it("sub-composition host in master view: styles locked but STILL croppable (viewport clip)", () => {
    const a = resolveEditingAffordances(baseFacts({ isCompositionHost: true, isMasterView: true }));
    expect(a.capabilities.canEditStyles).toBe(false); // internals are edited by drilling in
    expect(a.capabilities.canCrop).toBe(true); // crop is a viewport clip on the host in the parent source
  });

  it("script-generated / locked elements are not croppable", () => {
    expect(
      resolveEditingAffordances(baseFacts({ existsInSource: false })).capabilities.canCrop,
    ).toBe(false);
    expect(
      resolveEditingAffordances(baseFacts({ isInsideLockedComposition: true })).capabilities
        .canCrop,
    ).toBe(false);
  });

  it("absolute + left/top + identity transform: canMove", () => {
    const a = resolveEditingAffordances(
      baseFacts({
        computedStyles: { position: "absolute", left: "10px", top: "20px", transform: "none" },
      }),
    );
    expect(a.capabilities.canMove).toBe(true);
  });

  it("transform-driven geometry blocks canMove", () => {
    const a = resolveEditingAffordances(
      baseFacts({
        computedStyles: {
          position: "absolute",
          left: "10px",
          top: "20px",
          transform: "matrix(1,0,0,1,5,5)",
        },
      }),
    );
    expect(a.capabilities.canMove).toBe(false);
  });

  it("canResize requires canMove plus a width/height", () => {
    const a = resolveEditingAffordances(
      baseFacts({
        computedStyles: {
          position: "absolute",
          left: "0px",
          top: "0px",
          width: "100px",
          transform: "none",
        },
      }),
    );
    expect(a.capabilities.canResize).toBe(true);
  });

  it("inline left/top override missing computed", () => {
    const a = resolveEditingAffordances(
      baseFacts({
        inlineStyles: { left: "5px", top: "5px" },
        computedStyles: { position: "fixed", transform: "none" },
      }),
    );
    expect(a.capabilities.canMove).toBe(true);
  });

  it("computedStyles absent: canMove/canResize default false", () => {
    const a = resolveEditingAffordances(
      baseFacts({ computedStyles: undefined, inlineStyles: { left: "5px", top: "5px" } }),
    );
    expect(a.capabilities.canMove).toBe(false);
    expect(a.capabilities.canResize).toBe(false);
  });

  it("composition host + master view: no edit styles, geometry blocked", () => {
    const a = resolveEditingAffordances(baseFacts({ isCompositionHost: true, isMasterView: true }));
    expect(a.capabilities.canEditStyles).toBe(false);
    expect(a.capabilities.canApplyManualOffset).toBe(false);
    expect(a.capabilities.reasonIfDisabled).toContain("internal layer");
  });
});

describe("resolveEditingAffordances — sections", () => {
  it("video: media + colorGrading", () => {
    const s = resolveEditingAffordances(baseFacts({ tag: "video" })).sections;
    expect(s).toMatchObject({ media: true, colorGrading: true });
  });

  it("audio: media but not colorGrading", () => {
    const s = resolveEditingAffordances(baseFacts({ tag: "audio" })).sections;
    expect(s).toMatchObject({ media: true, colorGrading: false });
  });

  it("img: media + colorGrading", () => {
    const s = resolveEditingAffordances(baseFacts({ tag: "img" })).sections;
    expect(s).toMatchObject({ media: true, colorGrading: true });
  });

  it("editable text on a plain element: text section", () => {
    const s = resolveEditingAffordances(baseFacts({ hasEditableText: true })).sections;
    expect(s.text).toBe(true);
  });

  it("text section suppressed on host / locked", () => {
    expect(
      resolveEditingAffordances(baseFacts({ hasEditableText: true, isCompositionHost: true }))
        .sections.text,
    ).toBe(false);
    expect(
      resolveEditingAffordances(
        baseFacts({ hasEditableText: true, isInsideLockedComposition: true }),
      ).sections.text,
    ).toBe(false);
  });

  it("data-start drives timing; animations drive timing + animation", () => {
    expect(resolveEditingAffordances(baseFacts({ hasTimingStart: true })).sections.timing).toBe(
      true,
    );
    const anim = resolveEditingAffordances(baseFacts({ animationCount: 2 })).sections;
    expect(anim).toMatchObject({ timing: true, animation: true });
  });
});

describe("resolveEditingSections (sections-only export)", () => {
  it("matches resolveEditingAffordances().sections for the same facts", () => {
    const facts = baseFacts({ tag: "video", hasEditableText: true, animationCount: 1 });
    expect(resolveEditingSections(facts)).toEqual(resolveEditingAffordances(facts).sections);
  });

  it("animationCount > 0 turns on timing + animation even without data-start", () => {
    const s = resolveEditingSections(baseFacts({ hasTimingStart: false, animationCount: 3 }));
    expect(s).toMatchObject({ timing: true, animation: true });
  });
});
