// FilterPanel — unit tests (Issue #274).
//
// NOTE: This project has no @testing-library/react / jsdom configured (see
// PriceFetchControl.test.tsx). FilterPanel, unlike most components with
// their own test file, has NO internal state or hooks besides `useTranslation`
// — mocked below into a plain function — so it can be invoked directly as a
// plain function (bypassing JSX/React.createElement's usual `<FilterPanel />`
// call site) and its return value (a plain React-element object tree, since
// JSX creation never touches the DOM) walked directly. This gives genuine
// coverage of the shipped render + selection-wiring code, not a
// re-implementation of its logic in the test.

import { describe, it, expect, vi } from "vitest";

vi.mock("react-i18next", () => ({
  useTranslation: vi.fn(() => ({
    t: (key: string) => key,
    i18n: { language: "fr" },
  })),
}));

vi.mock("lucide-react", () => ({
  Inbox: () => null,
}));

import FilterPanel, { toggleAccountId } from "./FilterPanel";
import type { FilterPanelProps } from "./FilterPanel";
import type { ImportSource } from "../../shared/types";

// ---------------------------------------------------------------------------
// Minimal React-element tree walker (no renderer/DOM involved)
// ---------------------------------------------------------------------------

interface ElementNode {
  type: unknown;
  props: Record<string, unknown> & { children?: unknown };
}

type FlatNode = ElementNode | string | number;

function isElementNode(x: unknown): x is ElementNode {
  return typeof x === "object" && x !== null && "props" in x;
}

function flatten(node: unknown): FlatNode[] {
  if (Array.isArray(node)) return node.flatMap(flatten);
  if (node === null || node === undefined || typeof node === "boolean") return [];
  if (typeof node === "string" || typeof node === "number") return [node];
  if (isElementNode(node)) return [node, ...flatten(node.props.children)];
  return [];
}

function findCheckboxes(root: unknown): ElementNode[] {
  return flatten(root).filter(
    (n): n is ElementNode => isElementNode(n) && n.props.type === "checkbox",
  );
}

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

const SOURCE_CHEQUING: ImportSource = {
  id: 1,
  name: "Compte chèque Desjardins",
  date_format: "YYYY-MM-DD",
  delimiter: ",",
  encoding: "utf-8",
  column_mapping: "{}",
  skip_lines: 0,
  has_header: true,
  amount_mode: "single",
  sign_convention: "negative_expense",
  created_at: "2026-01-01",
  updated_at: "2026-01-01",
};

const SOURCE_VISA: ImportSource = {
  ...SOURCE_CHEQUING,
  id: 2,
  name: "Visa Desjardins",
};

function renderPanel(overrides: Partial<FilterPanelProps> = {}) {
  const onAccountIdsChange = vi.fn();
  const props: FilterPanelProps = {
    temporalControl: "TEMPORAL_CONTROL_MARKER",
    accountIds: [],
    onAccountIdsChange,
    accounts: [SOURCE_CHEQUING, SOURCE_VISA],
    ...overrides,
  };
  const element = FilterPanel(props);
  return { element, onAccountIdsChange };
}

// ---------------------------------------------------------------------------
// toggleAccountId — pure selection logic
// ---------------------------------------------------------------------------

describe("toggleAccountId", () => {
  it("adds an absent id to an empty list", () => {
    expect(toggleAccountId([], 1)).toEqual([1]);
  });

  it("appends an absent id after the existing ones", () => {
    expect(toggleAccountId([1, 2], 3)).toEqual([1, 2, 3]);
  });

  it("removes a present id, preserving the order of the remaining ids", () => {
    expect(toggleAccountId([1, 2, 3], 2)).toEqual([1, 3]);
  });

  it("removing the only selected id returns an empty array (= all accounts)", () => {
    expect(toggleAccountId([1], 1)).toEqual([]);
  });

  it("does not mutate the input array", () => {
    const input = [1, 2];
    toggleAccountId(input, 3);
    expect(input).toEqual([1, 2]);
  });
});

// ---------------------------------------------------------------------------
// FilterPanel — render
// ---------------------------------------------------------------------------

describe("FilterPanel — render", () => {
  it("renders the injected temporalControl as-is", () => {
    const { element } = renderPanel();
    expect(flatten(element)).toContain("TEMPORAL_CONTROL_MARKER");
  });

  it("renders one checkbox per account", () => {
    const { element } = renderPanel();
    expect(findCheckboxes(element)).toHaveLength(2);
  });

  it("renders each account's name as visible text", () => {
    const { element } = renderPanel();
    const text = flatten(element);
    expect(text).toContain("Compte chèque Desjardins");
    expect(text).toContain("Visa Desjardins");
  });

  it("checks exactly the accounts present in accountIds", () => {
    const { element } = renderPanel({ accountIds: [2] });
    const checkboxes = findCheckboxes(element);
    expect(checkboxes.map((c) => c.props.checked)).toEqual([false, true]);
  });

  it("checks no checkbox when accountIds is empty (= all accounts, no explicit filter)", () => {
    const { element } = renderPanel({ accountIds: [] });
    const checkboxes = findCheckboxes(element);
    expect(checkboxes.every((c) => c.props.checked === false)).toBe(true);
  });

  it("renders zero checkboxes when accounts is empty, without crashing", () => {
    const { element } = renderPanel({ accounts: [] });
    expect(findCheckboxes(element)).toHaveLength(0);
  });

  it("uses i18n keys for the accounts filter label and hint (never a hardcoded string)", () => {
    const { element } = renderPanel();
    const text = flatten(element);
    expect(text).toContain("reports.filters.accounts.label");
    expect(text).toContain("reports.filters.accounts.hint");
  });
});

// ---------------------------------------------------------------------------
// FilterPanel — selection wiring
// ---------------------------------------------------------------------------

describe("FilterPanel — selection wiring", () => {
  it("checking an unselected account calls onAccountIdsChange with it added", () => {
    const { element, onAccountIdsChange } = renderPanel({ accountIds: [1] });
    const checkboxes = findCheckboxes(element);
    // accounts[1] = SOURCE_VISA (id 2), currently unselected
    (checkboxes[1].props.onChange as () => void)();
    expect(onAccountIdsChange).toHaveBeenCalledWith([1, 2]);
  });

  it("unchecking the only selected account calls onAccountIdsChange with an empty array", () => {
    const { element, onAccountIdsChange } = renderPanel({ accountIds: [1] });
    const checkboxes = findCheckboxes(element);
    // accounts[0] = SOURCE_CHEQUING (id 1), currently selected
    (checkboxes[0].props.onChange as () => void)();
    expect(onAccountIdsChange).toHaveBeenCalledWith([]);
  });

  it("checking a second account preserves the first (multi-select)", () => {
    const { element, onAccountIdsChange } = renderPanel({ accountIds: [] });
    const checkboxes = findCheckboxes(element);
    (checkboxes[0].props.onChange as () => void)();
    (checkboxes[1].props.onChange as () => void)();
    expect(onAccountIdsChange).toHaveBeenNthCalledWith(1, [1]);
    expect(onAccountIdsChange).toHaveBeenNthCalledWith(2, [2]);
  });
});
