import {
  html,
  LitElement,
  render as litRender,
  type TemplateResult,
} from "lit";
import { assert, beforeEach, describe, test } from "vitest";
import { EFTimegroup } from "./EFTimegroup.js";
import "./EFTimegroup.js";
import { customElement } from "lit/decorators/custom-element.js";
import { ContextMixin } from "../gui/ContextMixin.js";
import { EFTemporal } from "./EFTemporal.js";
// Need workbench to make workbench wrapping occurs
import "../gui/EFWorkbench.js";

beforeEach(() => {
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (typeof key !== "string") continue;
    localStorage.removeItem(key);
  }
  while (document.body.children.length) {
    document.body.children[0]?.remove();
  }
});

@customElement("test-context")
class TestContext extends ContextMixin(LitElement) {}

@customElement("test-temporal")
class TestTemporal extends EFTemporal(LitElement) {
  get hasOwnDuration(): boolean {
    return true;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "test-temporal": TestTemporal;
    "test-context": TestContext;
  }
}

const renderTimegroup = (result: TemplateResult) => {
  const container = document.createElement("div");
  litRender(result, container);
  const firstChild = container.firstElementChild;
  if (!firstChild) {
    throw new Error("No first child found");
  }
  if (!(firstChild instanceof EFTimegroup)) {
    throw new Error("First child is not an EFTimegroup");
  }
  document.body.appendChild(firstChild);
  return firstChild;
};

describe(`<ef-timegroup mode='fit'>`, () => {
  test("duration is zero when there is no parent to fit into", () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="fit"></ef-timegroup>`,
    );
    assert.equal(timegroup.durationMs, 0);
  });

  test("duration is zero when there is no parent to fit into, even if there are children with duration", () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="fit">
        <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
      </ef-timegroup>`,
    );
    assert.equal(timegroup.durationMs, 0);
  });

  test("duration is the duration of the parent timegroup", () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="fixed" duration="10s">
        <ef-timegroup id="child" mode="fit"></ef-timegroup>
      </ef-timegroup>`,
    );
    const child = timegroup.querySelector("#child") as EFTimegroup;
    assert.equal(child.durationMs, 10_000);
  });

  test("fit mode items inside a sequence are given zero duration and do not factor into the duration of the sequence", () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="sequence">
        <ef-timegroup id="child" mode="fit"></ef-timegroup>
      </ef-timegroup>`,
    );
    const child = timegroup.querySelector("#child") as EFTimegroup;
    assert.equal(child.durationMs, 0);
    assert.equal(timegroup.durationMs, 0);
  });

  test("fit mode can be used to constrain a 'background' timegroup into a 'foreground' sequence", async () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup mode="contain">
          <ef-timegroup id="foreground" mode="sequence">
            <ef-timegroup mode="fixed" duration="10s"></ef-timegroup>
            <ef-timegroup mode="fixed" duration="10s"></ef-timegroup>
          </ef-timegroup>
          <ef-timegroup id="background" mode="fit">
            <ef-timegroup mode="fixed" duration="30s"></ef-timegroup>
          </ef-timegroup>
        </ef-timegroup>
      `,
    );

    const foreground = timegroup.querySelector("#foreground") as EFTimegroup;
    const background = timegroup.querySelector("#background") as EFTimegroup;
    assert.equal(foreground.durationMs, 20_000);
    assert.equal(background.durationMs, 20_000);
  });
});

describe(`<ef-timegroup mode="fixed">`, () => {
  test("can explicitly set a duration in seconds", async () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
    );
    assert.equal(timegroup.durationMs, 10_000);
  });

  test("can explicitly set a duration in milliseconds", async () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="fixed" duration="10ms"></ef-timegroup>`,
    );
    assert.equal(timegroup.durationMs, 10);
  });
});

describe(`<ef-timegroup mode="sequence">`, () => {
  test("fixed duration is ignored", () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="sequence" duration="10s"></ef-timegroup>`,
    );
    assert.equal(timegroup.durationMs, 0);
  });

  test("duration is the sum of child time groups", async () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup mode="sequence">
          <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
          <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
        </ef-timegroup>
      `,
    );
    assert.equal(timegroup.durationMs, 10_000);
  });

  test("duration can include any element with a durationMs value", async () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup mode="sequence">
          <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
          <test-temporal duration="5s"></test-temporal>
        </ef-timegroup>
      `,
    );

    assert.equal(timegroup.durationMs, 10_000);
  });

  test("arbitrary html does not factor into the calculation of a sequence duration", () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup mode="sequence">
          <div>
            <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
          </div>
        </ef-timegroup>
      `,
    );
    assert.equal(timegroup.durationMs, 5_000);
  });

  test("nested time groups do not factor into the calculation of a sequence duration", async () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup mode="sequence">
          <ef-timegroup mode="fixed" duration="5s">
            <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
          </ef-timegroup>
        </ef-timegroup>
      `,
    );
    assert.equal(timegroup.durationMs, 5_000);
  });
});

describe(`<ef-timegroup mode="contain">`, () => {
  test("fixed duration is ignored", () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="contain" duration="10s"></ef-timegroup>`,
    );
    assert.equal(timegroup.durationMs, 0);
  });

  test("duration is the maximum of it's child time groups", async () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup mode="contain">
          <ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
          <ef-timegroup mode="fixed" duration="10s"></ef-timegroup>
        </ef-timegroup>
      `,
    );
    assert.equal(timegroup.durationMs, 10_000);
  });
});

describe("startTimeMs", () => {
  test("is computed relative to the root time group", async () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup id="root" mode="sequence">
        <ef-timegroup id="a" mode="fixed" duration="5s"></ef-timegroup>
        <ef-timegroup id="b" mode="sequence">
          <ef-timegroup id="c" mode="fixed" duration="5s"></ef-timegroup>
          <ef-timegroup id="d" mode="contain">
            <ef-timegroup id="e" mode="fixed" duration="5s"></ef-timegroup>
            <ef-timegroup id="f" mode="fixed" duration="5s"></ef-timegroup>
          </ef-timegroup>
        </ef-timegroup>
      </ef-timegroup>`,
    );

    const a = timegroup.querySelector("#a") as EFTimegroup;
    const b = timegroup.querySelector("#b") as EFTimegroup;
    const c = timegroup.querySelector("#c") as EFTimegroup;
    const d = timegroup.querySelector("#d") as EFTimegroup;
    const e = timegroup.querySelector("#e") as EFTimegroup;
    const f = timegroup.querySelector("#f") as EFTimegroup;

    assert.equal(a.startTimeMs, 0);
    assert.equal(b.startTimeMs, 5_000);
    assert.equal(c.startTimeMs, 5_000);
    assert.equal(d.startTimeMs, 10_000);
    assert.equal(e.startTimeMs, 10_000);
    assert.equal(f.startTimeMs, 10_000);
  });

  //   // TODO: Rethink offset math, it shouldn't effect the duration of a temporal item
  //   // but actually change the start and end time.
  //   litTest.skip("can be offset with offset attribute", async ({ container }) => {
  //     render(
  //       html`<ef-timegroup id="root" mode="contain">
  //         <test-temporal id="a" duration="5s" offset="5s"></test-temporal>
  //       </ef-timegroup> `,
  //       container,
  //     );

  //     const root = container.querySelector("#root") as EFTimegroup;
  //     const a = container.querySelector("#a") as TestTemporal;

  //     assert.equal(a.durationMs, 5_000);
  //     assert.equal(root.durationMs, 10_000);
  //     assert.equal(a.startTimeMs, 5_000);
  //   });

  // litTest.skip(
  //   "offsets do not affect start time when in a sequence group",
  //   async ({ container }) => {
  //     render(
  //       html`<ef-timegroup id="root" mode="sequence">
  //         <test-temporal id="a" duration="5s"></test-temporal>
  //         <test-temporal id="b" duration="5s" offset="5s"></test-temporal>
  //       </ef-timegroup> `,
  //       container,
  //     );

  //     const root = container.querySelector("#root") as EFTimegroup;
  //     const a = container.querySelector("#a") as TestTemporal;
  //     const b = container.querySelector("#b") as TestTemporal;

  //     assert.equal(root.durationMs, 10_000);
  //     assert.equal(a.startTimeMs, 0);
  //     assert.equal(b.startTimeMs, 5_000);
  //   },
  // );

  test("temporal elements default to expand to fill a timegroup", async () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup id="root" mode="fixed" duration="10s">
        <test-temporal id="a"></test-temporal>
      </ef-timegroup> `,
    );

    const a = timegroup.querySelector("#a") as TestTemporal;

    assert.equal(timegroup.durationMs, 10_000);
    assert.equal(a.durationMs, 10_000);
  });

  test("element's parentTimegroup updates as they move", async () => {
    const container = document.createElement("div");
    document.body.appendChild(container);
    const timegroup1 = document.createElement("ef-timegroup");
    timegroup1.setAttribute("mode", "fixed");
    timegroup1.setAttribute("duration", "5s");

    const timegroup2 = document.createElement("ef-timegroup");
    timegroup2.setAttribute("mode", "fixed");
    timegroup2.setAttribute("duration", "5s");

    container.appendChild(timegroup1);
    container.appendChild(timegroup2);

    const temporal = document.createElement("test-temporal");

    timegroup1.appendChild(temporal);

    assert.equal(temporal.parentTimegroup, timegroup1);

    timegroup2.appendChild(temporal);
    assert.equal(temporal.parentTimegroup, timegroup2);
  });

  test("elements can access their root temporal element", async () => {
    const root = renderTimegroup(
      html`<ef-timegroup id="root" mode="contain" duration="10s">
        <ef-timegroup id="a" mode="contain">
          <div>
            <ef-timegroup id="b" mode="contain">
              <div>
                <test-temporal id="c" duration="5s"></test-temporal>
              </div>
            </ef-timegroup>
          </div>
        </ef-timegroup>
      </ef-timegroup> `,
    );

    const a = root.querySelector("#a") as EFTimegroup;
    const b = root.querySelector("#b") as EFTimegroup;
    const c = root.querySelector("#c") as TestTemporal;

    assert.equal(root.rootTimegroup, root);

    assert.equal(a.rootTimegroup, root);
    assert.equal(b.rootTimegroup, root);
    assert.equal(c.rootTimegroup, root);
  });
});

describe("setting currentTime", () => {
  test("persists in localStorage if the timegroup has an id and is in the dom", async () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup id="localStorage-test" mode="fixed" duration="10s"></ef-timegroup>`,
    );
    localStorage.removeItem(timegroup.storageKey);
    document.body.appendChild(timegroup);
    await timegroup.waitForMediaDurations();

    timegroup.currentTime = 5_000; // 5000 seconds, should clamp to 10s
    await timegroup.seekTask.taskComplete;

    const storedValue = localStorage.getItem(timegroup.storageKey);
    assert.equal(storedValue, "10"); // Should store 10 (clamped from 5000 to duration)
    timegroup.remove();
  });

  test("does not persist in localStorage if the timegroup has no id", async () => {
    const timegroup = renderTimegroup(
      html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
    );
    document.body.appendChild(timegroup);
    timegroup.currentTime = 5_000;
    timegroup.removeAttribute("id");
    assert.throws(() => {
      assert.isNull(localStorage.getItem(timegroup.storageKey));
    }, "Timegroup must have an id to use localStorage");
    timegroup.remove();
  });

  test("nested items derive their ownCurrentTimeMs", async () => {
    const timegroup = renderTimegroup(
      html`
        <ef-timegroup id="root" mode="sequence">
          <ef-timegroup id="a" mode="fixed" duration="5s"> </ef-timegroup>
          <ef-timegroup id="b" mode="fixed" duration="5s"> </ef-timegroup>
        </ef-timegroup>
      `,
    );

    const root = timegroup;
    const a = timegroup.querySelector("#a") as EFTimegroup;
    const b = timegroup.querySelector("#b") as EFTimegroup;

    assert.equal(a.ownCurrentTimeMs, 0);
    assert.equal(b.ownCurrentTimeMs, 0);

    root.currentTimeMs = 2_500;

    assert.equal(a.ownCurrentTimeMs, 2_500);
    assert.equal(b.ownCurrentTimeMs, 0);

    // Wait for frame update to complete before next assignment
    await new Promise((resolve) => setTimeout(resolve, 10));

    root.currentTimeMs = 7_500;

    assert.equal(a.ownCurrentTimeMs, 5_000);
    assert.equal(b.ownCurrentTimeMs, 2_500);
  });
});

describe("shouldWrapWithWorkbench", () => {
  test.skip("should not wrap if EF_INTERACTIVE is false", () => {
    // TODO: need a way to define EF_INTERACTIVE in a test
  });

  test("should wrap if root-most timegroup", () => {
    const root = document.createElement("ef-timegroup");
    const child = document.createElement("ef-timegroup");
    root.appendChild(child);
    assert.isTrue(child.shouldWrapWithWorkbench());
  });

  test("should not wrap if contained within a preview context", () => {
    const timegorup = document.createElement("ef-timegroup");
    const context = document.createElement("test-context");
    context.append(timegorup);
    assert.isFalse(timegorup.shouldWrapWithWorkbench());
  });
});

describe("DOM nodes", () => {
  test("can have mode and duration set as attributes", () => {
    const timegroup = document.createElement("ef-timegroup");
    timegroup.setAttribute("mode", "fixed");
    timegroup.setAttribute("duration", "10s");
    assert.equal(timegroup.mode, "fixed");
    assert.equal(timegroup.durationMs, 10_000);
  });
});
