// 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 LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts";
import {type cell, useCell} from "../../../src/utils/cellStore.ts";
import { StrictMode } from "react";

jest.mock("../../../src/utils/priorityFiltering.ts", () => ({
  applyPriorityPredicates: jest.fn((_log, preds: any[]) =>
    preds.every(() => true) // default: pass all
  ),
}));
import {applyPriorityPredicates} from "../../../src/utils/priorityFiltering.ts";

class MockEventSource {
  url: string;
  onmessage: ((event: { data: string }) => void) | null = null;
  onerror: ((event: unknown) => void) | null = null;
  close = jest.fn();

  constructor(url: string) {
    this.url = url;
    // expose the latest instance for tests:
    (globalThis as any).__es = this;
  }
}

beforeAll(() => {
  globalThis.EventSource = MockEventSource as any;
});

afterEach(() => {
  // reset mock so previous instance not reused accidentally
  (globalThis as any).__es = undefined;
  jest.clearAllMocks();
});

function LogsProbe({ filters }: { filters: Map<string, any> }) {
  const { filteredLogs, distinctNames } = useLogs(filters);

  return (
    <div>
      <div data-testid="names-count">{distinctNames.size}</div>
      <ul data-testid="logs">
        {filteredLogs.map((c, i) => (
          <LogItem key={i} cell={c} index={i} />
        ))}
      </ul>
    </div>
  );
}

function LogItem({ cell: c, index }: { cell: ReturnType<typeof cell<LogRecord>>; index: number }) {
  const value = useCell(c);
  return (
    <li data-testid={`log-${index}`}>
      <span data-testid={`log-${index}-name`}>{value.name}</span>
      <span data-testid={`log-${index}-msg`}>{value.message}</span>
      <span data-testid={`log-${index}-first`}>{String(value.firstCreated)}</span>
      <span data-testid={`log-${index}-created`}>{String(value.created)}</span>
      <span data-testid={`log-${index}-ref`}>{value.reference ?? ""}</span>
    </li>
  );
}

function emit(log: LogRecord) {
  const eventSource = (globalThis as any).__es as MockEventSource;
  if (!eventSource || !eventSource.onmessage) throw new Error("EventSource not initialized");
  act(() => {
    eventSource.onmessage!({ data: JSON.stringify(log) });
  });
}

describe("useLogs (unit)", () => {
  it("creates EventSource once and closes on unmount", () => {
    const filters = new Map(); // allow all by default
    const { unmount } = render(
      <StrictMode>
        <LogsProbe filters={filters} />
      </StrictMode>
    );
    const es = (globalThis as any).__es as MockEventSource;
    expect(es).toBeTruthy();
    expect(es.url).toBe("http://localhost:8000/logs/stream");

    unmount();
    expect(es.close).toHaveBeenCalledTimes(1);
  });

  it("appends filtered logs and collects distinct names", () => {
    const filters = new Map();
    render(
      <StrictMode>
        <LogsProbe filters={filters} />
      </StrictMode>
    );

    expect(screen.getByTestId("names-count")).toHaveTextContent("0");

    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "alpha",
      message: "m1",
      created: 1,
      relativeCreated: 1,
      firstCreated: 1,
      firstRelativeCreated: 1,
    });
    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "beta",
      message: "m2",
      created: 2,
      relativeCreated: 2,
      firstCreated: 2,
      firstRelativeCreated: 2,
    });
    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "alpha",
      message: "m3",
      created: 3,
      relativeCreated: 3,
      firstCreated: 3,
      firstRelativeCreated: 3,
    });

    // 3 messages (no reference), 2 distinct names
    expect(screen.getAllByRole("listitem")).toHaveLength(3);
    expect(screen.getByTestId("names-count")).toHaveTextContent("2");

    expect(screen.getByTestId("log-0-name")).toHaveTextContent("alpha");
    expect(screen.getByTestId("log-1-name")).toHaveTextContent("beta");
    expect(screen.getByTestId("log-2-name")).toHaveTextContent("alpha");
  });

  it("updates first message with reference when a second one with that reference comes", () => {
    const filters = new Map();
    render(<LogsProbe filters={filters} />);

    // First message with ref r1
    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "svc",
      message: "first",
      reference: "r1",
      created: 10,
      relativeCreated: 10,
      firstCreated: 10,
      firstRelativeCreated: 10,
    });

    // Second message with same ref r1, should still be a single item
    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "svc",
      message: "second",
      reference: "r1",
      created: 20,
      relativeCreated: 20,
      firstCreated: 20,
      firstRelativeCreated: 20,
    });

    const items = screen.getAllByRole("listitem");
    expect(items).toHaveLength(1);

    // Same single item, but message should be "second"
    expect(screen.getByTestId("log-0-msg")).toHaveTextContent("second");
    // The "firstCreated" should remain the original (10), while "created" is now 20
    expect(screen.getByTestId("log-0-first")).toHaveTextContent("10");
    expect(screen.getByTestId("log-0-created")).toHaveTextContent("20");
    expect(screen.getByTestId("log-0-ref")).toHaveTextContent("r1");
  });

  it("runs recomputeFiltered when filters change", () => {
    const allowAll = new Map<string, any>();
    const { rerender } = render(<LogsProbe filters={allowAll} />);

    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "n1",
      message: "ok",
      created: 1,
      relativeCreated: 1,
      firstCreated: 1,
      firstRelativeCreated: 1,
    });
    emit({
      levelname: "DEBUG",
      levelno: 10,
      name: "n2",
      message: "ok",
      created: 2,
      relativeCreated: 2,
      firstCreated: 2,
      firstRelativeCreated: 2,
    });
    emit({
      levelname: "INFO",
      levelno: 20,
      name: "n3",
      message: "ok1",
      reference: "r1",
      created: 3,
      relativeCreated: 3,
      firstCreated: 3,
      firstRelativeCreated: 3,
    });
    emit({
      levelname: "INFO",
      levelno: 20,
      name: "n3",
      message: "ok2",
      reference: "r1",
      created: 4,
      relativeCreated: 4,
      firstCreated: 4,
      firstRelativeCreated: 4,
    });

    expect(screen.getAllByRole("listitem")).toHaveLength(3);

    // Now change filters to block all < INFO
    (applyPriorityPredicates as jest.Mock).mockImplementation((l) => l.levelno >= 20);
    const blockDebug = new Map<string, any>([["dummy", { value: true }]]);
    rerender(<LogsProbe filters={blockDebug} />);

    // Should recompute with shorter list
    expect(screen.queryAllByRole("listitem")).toHaveLength(1);

    // Switch back to allow-all
    (applyPriorityPredicates as jest.Mock).mockImplementation((_log, preds: any[]) =>
      preds.every(() => true)
    );
    rerender(<LogsProbe filters={allowAll} />);

    // recompute should restore all three
    expect(screen.getAllByRole("listitem")).toHaveLength(3);
  });
});
