// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {render, screen, act} from "@testing-library/react";
import "@testing-library/jest-dom";
import {type Cell, cell, useCell} from "../../src/utils/cellStore.ts";

describe("cell store (unit)", () => {
  it("returns initial value with get()", () => {
    const c = cell(123);
    expect(c.get()).toBe(123);
  });

  it("updates value with set(next)", () => {
    const c = cell("a");
    c.set("b");
    expect(c.get()).toBe("b");
  });

  it("gives previous value in set(updater)", () => {
    const c = cell(1);
    c.set((prev) => prev + 2);
    expect(c.get()).toBe(3);
  });

  it("calls subscribe callback on set", () => {
    const c = cell(0);
    const cb = jest.fn();
    const unsub = c.subscribe(cb);

    c.set(1);
    c.set(2);

    expect(cb).toHaveBeenCalledTimes(2);
    unsub();
  });

  it("stops notifications when unsubscribing", () => {
    const c = cell(0);
    const cb = jest.fn();
    const unsub = c.subscribe(cb);

    c.set(1);
    unsub();
    c.set(2);

    expect(cb).toHaveBeenCalledTimes(1);
  });

  it("updates multiple listeners", () => {
    const c = cell("x");
    const a = jest.fn();
    const b = jest.fn();
    const ua = c.subscribe(a);
    const ub = c.subscribe(b);

    c.set("y");
    expect(a).toHaveBeenCalledTimes(1);
    expect(b).toHaveBeenCalledTimes(1);

    ua();
    ub();
  });
});

describe("cell store (integration)", () => {
  function View({c, label}: { c: Cell<any>; label: string }) {
    const v = useCell(c);
    // count renders to verify re-render behavior
    (View as any).__renders = ((View as any).__renders ?? 0) + 1;
    return <div data-testid={label}>{String(v)}</div>;
  }

  it("reads initial value and updates on set", () => {
    const c = cell("hello");

    render(<View c={c} label="value"/>);

    expect(screen.getByTestId("value")).toHaveTextContent("hello");

    act(() => {
      c.set("world");
    });

    expect(screen.getByTestId("value")).toHaveTextContent("world");
  });

  it("triggers one re-render with set", () => {
    const c = cell(1);
    (View as any).__renders = 0;

    render(<View c={c} label="num"/>);

    const rendersAfterMount = (View as any).__renders;

    act(() => {
      c.set((prev: number) => prev + 1);
    });

    // exactly one extra render from the update
    expect((View as any).__renders).toBe(rendersAfterMount + 1);
    expect(screen.getByTestId("num")).toHaveTextContent("2");
  });

  it("unsubscribes on unmount (no errors on later sets)", () => {
    const c = cell("a");

    const {unmount} = render(<View c={c} label="value"/>);

    unmount();

    // should not throw even though there was a subscriber
    expect(() =>
      act(() => {
        c.set("b");
      })
    ).not.toThrow();
  });

  it("only re-renders components that use the cell", () => {
    const a = cell("A");
    const b = cell("B");

    let rendersA = 0;
    let rendersB = 0;

    function A() {
      const v = useCell(a);
      rendersA++;
      return <div data-testid="A">{v}</div>;
    }

    function B() {
      const v = useCell(b);
      rendersB++;
      return <div data-testid="B">{v}</div>;
    }

    render(
      <>
        <A/>
        <B/>
      </>
    );

    const rendersAAfterMount = rendersA;
    const rendersBAfterMount = rendersB;

    act(() => {
      a.set("A2"); // only A should update
    });

    expect(screen.getByTestId("A")).toHaveTextContent("A2");
    expect(screen.getByTestId("B")).toHaveTextContent("B");

    expect(rendersA).toBe(rendersAAfterMount + 1);
    expect(rendersB).toBe(rendersBAfterMount); // unchanged
  });
});
