// 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, waitFor, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as React from "react";

type ControlledUseState = typeof React.useState & {
  __forceNextReturn?: (value: any) => jest.Mock;
  __resetMockState?: () => void;
};

jest.mock("react", () => {
  const actual = jest.requireActual("react");
  const queue: Array<{value: any; setter: jest.Mock}> = [];
  const mockUseState = ((initial: any) => {
    if (queue.length) {
      const {value, setter} = queue.shift()!;
      return [value, setter];
    }
    return actual.useState(initial);
  }) as ControlledUseState;

  mockUseState.__forceNextReturn = (value: any) => {
    const setter = jest.fn();
    queue.push({value, setter});
    return setter;
  };
  mockUseState.__resetMockState = () => {
    queue.length = 0;
  };

  return {
    __esModule: true,
    ...actual,
    useState: mockUseState,
  };
});
import Filters from "../../../src/components/Logging/Filters.tsx";
import type {LogFilterPredicate, LogRecord} from "../../../src/components/Logging/useLogs.ts";

const GLOBAL = "global_log_level";
const AGENT_PREFIX = "agent_log_level_";
const optionMapping = new Map([
  ["ALL", 0],
  ["DEBUG", 10],
  ["INFO", 20],
  ["WARNING", 30],
  ["ERROR", 40],
  ["CRITICAL", 50],
  ["NONE", 999_999_999_999],
]);

const controlledUseState = React.useState as ControlledUseState;

afterEach(() => {
  controlledUseState.__resetMockState?.();
});

function getCallArg<T>(mock: jest.Mock, index = 0): T {
  return mock.mock.calls[index][0] as T;
}

function sampleRecord(levelno: number, name = "any.logger"): LogRecord {
  return {
    levelname: "UNKNOWN",
    levelno,
    name,
    message: "Whatever",
    created: 0,
    relativeCreated: 0,
    firstCreated: 0,
    firstRelativeCreated: 0,
  };
}

// --------------------------------------------------------------------------

describe("Filters", () => {
  describe("Global level filter", () => {
    it("initializes to INFO when missing", async () => {
      const setFilterPredicates = jest.fn();
      const filterPredicates = new Map<string, LogFilterPredicate>();

      const view = render(
        <Filters
          filterPredicates={filterPredicates}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set<string>()}
        />
      );

      // Effect sets default to INFO
      await waitFor(() => {
        expect(setFilterPredicates).toHaveBeenCalled();
      });

      const updater = getCallArg<(prev: Map<string, LogFilterPredicate>) => Map<string, LogFilterPredicate>>(setFilterPredicates);
      const newMap = updater(filterPredicates);
      const global = newMap.get(GLOBAL)!;

      expect(global.value).toBe("INFO");
      expect(global.priority).toBe(0);
      // Predicate gate at INFO (>= 20)
      expect(global.predicate(sampleRecord(10))).toBe(false);
      expect(global.predicate(sampleRecord(20))).toBe(true);

      // UI shows INFO selected after parent state updates
      view.rerender(
        <Filters
          filterPredicates={newMap}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set<string>()}
        />
      );

      const globalSelect = screen.getByLabelText("Global:");
      expect((globalSelect as HTMLSelectElement).value).toBe("INFO");
    });

    it("updates predicate when selecting a higher level", async () => {
      // Start with INFO already present
      const existing = new Map<string, LogFilterPredicate>([
        [
          GLOBAL,
          {
            value: "INFO",
            priority: 0,
            predicate: (r: any) => r.levelno >= optionMapping.get("INFO")!
          }
        ]
      ]);

      const setFilterPredicates = jest.fn();
      const user = userEvent.setup();

      render(
        <Filters
          filterPredicates={existing}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set<string>()}
        />
      );

      const select = screen.getByLabelText("Global:");
      await user.selectOptions(select, "ERROR");

      const updater = getCallArg<(prev: Map<string, LogFilterPredicate>) => Map<string, LogFilterPredicate>>(setFilterPredicates);
      const updated = updater(existing);
      const global = updated.get(GLOBAL)!;

      expect(global.value).toBe("ERROR");
      expect(global.priority).toBe(0);
      expect(global.predicate(sampleRecord(30))).toBe(false);
      expect(global.predicate(sampleRecord(40))).toBe(true);
    });
  });

  describe("Agent level filters", () => {
    it("adds an agent using the current global level when none specified", async () => {
      // Global set to WARNING
      const existing = new Map<string, LogFilterPredicate>([
        [
          GLOBAL,
          {
            value: "WARNING",
            priority: 0,
            predicate: (r: any) => r.levelno >= optionMapping.get("WARNING")!
          }
        ]
      ]);

      const setFilterPredicates = jest.fn();
      const user = userEvent.setup();

      render(
        <Filters
          filterPredicates={existing}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set<string>(["pepper.speech", "vision.agent"])}
        />
      );

      const addSelect = screen.getByLabelText("Add:");
      await user.selectOptions(addSelect, "pepper.speech");

      // Agent setter is functional: prev => next
      const updater = getCallArg<(prev: Map<string, LogFilterPredicate>) => Map<string, LogFilterPredicate>>(setFilterPredicates);
      const next = updater(existing);

      const key = AGENT_PREFIX + "pepper.speech";
      const agentPred = next.get(key)!;

      expect(agentPred.priority).toBe(1);
      expect(agentPred.value).toEqual({agentName: "pepper.speech", level: "WARNING"});
      // When agentName matches, enforce WARNING (>= 30)
      expect(agentPred.predicate(sampleRecord(20, "pepper.speech"))).toBe(false);
      expect(agentPred.predicate(sampleRecord(30, "pepper.speech"))).toBe(true);
      // Other agents -> null
      expect(agentPred.predicate(sampleRecord(999, "other"))).toBeNull();
    });

    it("changes an agent's level when its select is updated", async () => {
      // Prepopulate agent predicate at WARNING
      const key = AGENT_PREFIX + "pepper.speech";
      const existing = new Map<string, LogFilterPredicate>([
        [
          GLOBAL,
          {
            value: "INFO",
            priority: 0,
            predicate: (r: any) => r.levelno >= optionMapping.get("INFO")!
          }
        ],
        [
          key,
          {
            value: {agentName: "pepper.speech", level: "WARNING"},
            priority: 1,
            predicate: (r: any) => (r.name === "pepper.speech" ? r.levelno >= optionMapping.get("WARNING")! : null)
          }
        ]
      ]);

      const setFilterPredicates = jest.fn();
      const user = userEvent.setup();

      const element = render(
        <Filters
          filterPredicates={existing}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set(["pepper.speech"])}
        />
      );

      const agentSelect = element.container.querySelector("select#log_level_pepper\\.speech")!;

      await user.selectOptions(agentSelect, "ERROR");

      const updater = getCallArg<(prev: Map<string, LogFilterPredicate>) => Map<string, LogFilterPredicate>>(setFilterPredicates);
      const next = updater(existing);
      const updated = next.get(key)!;

      expect(updated.value).toEqual({agentName: "pepper.speech", level: "ERROR"});
      // Threshold moved to ERROR (>= 40)
      expect(updated.predicate(sampleRecord(30, "pepper.speech"))).toBe(false);
      expect(updated.predicate(sampleRecord(40, "pepper.speech"))).toBe(true);
    });

    it("deletes an agent predicate when clicking its name button", async () => {
      const key = AGENT_PREFIX + "pepper.speech";
      const existing = new Map<string, LogFilterPredicate>([
        [
          GLOBAL,
          {
            value: "INFO",
            priority: 0,
            predicate: (r: any) => r.levelno >= optionMapping.get("INFO")!
          }
        ],
        [
          key,
          {
            value: {agentName: "pepper.speech", level: "INFO"},
            priority: 1,
            predicate: (r: any) => (r.name === "pepper.speech" ? r.levelno >= optionMapping.get("INFO")! : null)
          }
        ]
      ]);

      const setFilterPredicates = jest.fn();
      const user = userEvent.setup();

      render(
        <Filters
          filterPredicates={existing}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set<string>(["pepper.speech"])}
        />
      );

      const deleteBtn = screen.getByRole("button", {name: "speech:"});
      await user.click(deleteBtn);

      const updater = getCallArg<(prev: Map<string, LogFilterPredicate>) => Map<string, LogFilterPredicate>>(setFilterPredicates);
      const next = updater(existing);
      expect(next.has(key)).toBe(false);
    });
  });

  describe("Filter popup behavior", () => {
    function renderWithPopupOpen() {
      const existing = new Map<string, LogFilterPredicate>([
        [
          GLOBAL,
          {
            value: "INFO",
            priority: 0,
            predicate: (r: any) => r.levelno >= optionMapping.get("INFO")!
          }
        ]
      ]);
      const setFilterPredicates = jest.fn();
      const forceNext = controlledUseState.__forceNextReturn;
      if (!forceNext) throw new Error("useState mock missing helper");
      const setOpen = forceNext(true);

      render(
        <Filters
          filterPredicates={existing}
          setFilterPredicates={setFilterPredicates}
          agentNames={new Set(["pepper.vision"])}
        />
      );

      return { setOpen };
    }

    it("closes the popup when clicking outside", () => {
      const { setOpen } = renderWithPopupOpen();
      fireEvent.mouseDown(document.body);
      expect(setOpen).toHaveBeenCalledWith(false);
    });

    it("closes the popup when pressing Escape", () => {
      const { setOpen } = renderWithPopupOpen();
      fireEvent.keyDown(document, { key: "Escape" });
      expect(setOpen).toHaveBeenCalledWith(false);
    });
  });
});
