import {render, screen, fireEvent, act, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import type {Cell} from "../../../src/utils/cellStore.ts";
import {cell} from "../../../src/utils/cellStore.ts";
import type {LogFilterPredicate, LogRecord} from "../../../src/components/Logging/useLogs.ts";

const mockFiltersRender = jest.fn();
const loggingStoreRef: { current: null | { setState: (state: Partial<LoggingSettingsState>) => void } } = { current: null };

type LoggingSettingsState = {
  showRelativeTime: boolean;
  setShowRelativeTime: (show: boolean) => void;
  scrollToBottom: boolean;
  setScrollToBottom: (scroll: boolean) => void;
};

jest.mock("zustand", () => {
  const actual = jest.requireActual("zustand");
  const actualCreate = actual.create;
  return {
    __esModule: true,
    ...actual,
    create: (...args: any[]) => {
      const store = actualCreate(...args);
      const state = store.getState();
      if ("setShowRelativeTime" in state && "setScrollToBottom" in state) {
        loggingStoreRef.current = store;
      }
      return store;
    },
  };
});

jest.mock("../../../src/components/Logging/Filters.tsx", () => {
  const React = jest.requireActual("react");
  return {
    __esModule: true,
    default: (props: any) => {
      mockFiltersRender(props);
      return React.createElement("div", {"data-testid": "filters-mock"}, "filters");
    },
  };
});

jest.mock("../../../src/components/Logging/useLogs.ts", () => {
  const actual = jest.requireActual("../../../src/components/Logging/useLogs.ts");
  return {
    __esModule: true,
    ...actual,
    useLogs: jest.fn(),
  };
});

import {useLogs} from "../../../src/components/Logging/useLogs.ts";
const mockUseLogs = useLogs as jest.MockedFunction<typeof useLogs>;

type LoggingComponent = typeof import("../../../src/components/Logging/Logging.tsx").default;
let Logging: LoggingComponent;

beforeAll(async () => {
  if (!Element.prototype.scrollIntoView) {
    Object.defineProperty(Element.prototype, "scrollIntoView", {
      configurable: true,
      writable: true,
      value: function () {},
    });
  }

  ({default: Logging} = await import("../../../src/components/Logging/Logging.tsx"));
});

beforeEach(() => {
  mockUseLogs.mockReset();
  mockFiltersRender.mockReset();
  mockUseLogs.mockReturnValue({filteredLogs: [], distinctNames: new Set()});
  resetLoggingStore();
});

afterEach(() => {
  jest.restoreAllMocks();
});

function resetLoggingStore() {
  loggingStoreRef.current?.setState({
    showRelativeTime: false,
    scrollToBottom: true,
  });
}

function makeRecord(overrides: Partial<LogRecord> = {}): LogRecord {
  return {
    name: "pepper.logger",
    message: "default",
    levelname: "INFO",
    levelno: 20,
    created: 1,
    relativeCreated: 1,
    firstCreated: 1,
    firstRelativeCreated: 1,
    ...overrides,
  };
}

function makeCell(overrides: Partial<LogRecord> = {}): Cell<LogRecord> {
  return cell(makeRecord(overrides));
}

describe("Logging component", () => {
  it("renders log messages and toggles the timestamp between absolute and relative view", async () => {
    const logCell = makeCell({
      name: "pepper.trace.logging",
      message: "Ping",
      levelname: "WARNING",
      levelno: 30,
      created: 1_700_000_000,
      relativeCreated: 12_345,
      firstCreated: 1_700_000_000,
      firstRelativeCreated: 12_345,
    });

    const names = new Set(["pepper.trace.logging"]);
    mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: names});

    jest.spyOn(Date.prototype, "toLocaleTimeString").mockReturnValue("ABS TIME");
    const user = userEvent.setup();

    render(<Logging/>);

    expect(screen.getByText("Logs")).toBeDefined();
    expect(screen.getByText("WARNING")).toBeDefined();
    expect(screen.getByText("logging")).toBeDefined();
    expect(screen.getByText("Ping")).toBeDefined();

    let timestamp = screen.queryByText("ABS TIME");
    if (!timestamp) {
      // if previous test left the store toggled, click once to show absolute time
      timestamp = screen.getByText("00:00:12.345");
      await user.click(timestamp);
      timestamp = screen.getByText("ABS TIME");
    }

    await user.click(timestamp);
    expect(screen.getByText("00:00:12.345")).toBeDefined();
  });

  it("shows the scroll-to-bottom button after a manual scroll and scrolls when clicked", async () => {
    const logs = [
      makeCell({message: "first", firstRelativeCreated: 1}),
      makeCell({message: "second", firstRelativeCreated: 2}),
    ];
    mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()});

    const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {});
    const user = userEvent.setup();
    const view = render(<Logging/>);

    expect(screen.queryByRole("button", {name: "Scroll to bottom"})).toBeNull();

    const scrollable = view.container.querySelector(".scroll-y");
    expect(scrollable).toBeTruthy();

    fireEvent.wheel(scrollable!);

    const button = await screen.findByRole("button", {name: "Scroll to bottom"});
    await user.click(button);

    expect(scrollSpy).toHaveBeenCalled();
    await waitFor(() => {
      expect(screen.queryByRole("button", {name: "Scroll to bottom"})).toBeNull();
    });
  });

  it("scrolls the last element into view when a log cell updates", async () => {
    const logCell = makeCell({message: "Initial", firstRelativeCreated: 42});
    mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()});

    const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {});
    render(<Logging/>);

    await waitFor(() => {
      expect(scrollSpy).toHaveBeenCalledTimes(1);
    });
    scrollSpy.mockClear();

    act(() => {
      const current = logCell.get();
      logCell.set({...current, message: "Updated"});
    });

    expect(screen.getByText("Updated")).toBeDefined();
    await waitFor(() => {
      expect(scrollSpy).toHaveBeenCalledTimes(1);
    });
  });

  it("passes filter state to Filters and re-invokes useLogs when predicates change", async () => {
    const distinct = new Set(["pepper.core"]);
    mockUseLogs.mockImplementation((_filters: Map<string, LogFilterPredicate>) => ({
      filteredLogs: [],
      distinctNames: distinct,
    }));

    render(<Logging/>);

    expect(mockFiltersRender).toHaveBeenCalledTimes(1);
    const firstProps = mockFiltersRender.mock.calls[0][0];
    expect(firstProps.agentNames).toBe(distinct);

    const initialMap = firstProps.filterPredicates;
    expect(initialMap).toBeInstanceOf(Map);
    expect(initialMap.size).toBe(0);
    expect(mockUseLogs).toHaveBeenCalledWith(initialMap);

    const updatedPredicate: LogFilterPredicate = {
      value: "custom",
      priority: 0,
      predicate: () => true,
    };

    act(() => {
      firstProps.setFilterPredicates((prev: Map<string, LogFilterPredicate>) => {
        const next = new Map(prev);
        next.set("custom", updatedPredicate);
        return next;
      });
    });

    await waitFor(() => {
      expect(mockUseLogs).toHaveBeenCalledTimes(2);
    });

    const nextFilters = mockUseLogs.mock.calls[1][0];
    expect(nextFilters.get("custom")).toBe(updatedPredicate);

    const secondProps = mockFiltersRender.mock.calls[mockFiltersRender.mock.calls.length - 1][0];
    expect(secondProps.filterPredicates).toBe(nextFilters);
  });
});
