// Unit tests for the FilterBar's computeEffective() overlay. These pin
// the contract that any chip the user clicks ends up as a predicate the
// server can see — the t-paliad-283 regression had four sources picking
// up zero narrowing for /views/any because the bar's chip click didn't
// produce a non-empty `filter.predicates` for that source.
//
// Run with `bun test`.

import { test, expect, describe } from "bun:test";
import { computeEffective } from "./index";
import type { FilterSpec, RenderSpec } from "../views/types";
import type { BarState } from "./types";

// Mirrors paliad.user_views row {slug: "any"} — the saved Custom View
// that triggered the t-paliad-283 regression report.
const ANY_VIEW_FILTER: FilterSpec = {
  version: 1,
  sources: ["deadline", "appointment", "project_event", "approval_request"],
  scope: { projects: { mode: "all_visible" } },
  time: { field: "auto", horizon: "past_30d" },
};

const ANY_VIEW_RENDER: RenderSpec = {
  shape: "list",
  list: { sort: "date_asc", density: "comfortable" },
};

describe("filter-bar/computeEffective — /views/any (all 4 sources)", () => {
  test("empty state leaves base spec intact (no overlays)", () => {
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, {});
    expect(eff.filter.sources).toEqual([
      "deadline", "appointment", "project_event", "approval_request",
    ]);
    expect(eff.filter.time).toEqual({ field: "auto", horizon: "past_30d" });
    // predicates may be {} (the bar zero-fills it) but never carries a
    // stray narrowing on any source — that would silently filter
    // results the user never asked to filter.
    for (const src of ANY_VIEW_FILTER.sources) {
      expect(eff.filter.predicates?.[src]).toBeUndefined();
    }
  });

  test("deadline_status chip narrows deadline predicate", () => {
    const state: BarState = { deadline_status: ["pending"] };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.predicates?.deadline?.status).toEqual(["pending"]);
  });

  test("appointment_type chip narrows appointment predicate", () => {
    const state: BarState = { appointment_type: ["hearing"] };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.predicates?.appointment?.appointment_types).toEqual(["hearing"]);
  });

  test("approval_viewer_role chip narrows approval predicate", () => {
    const state: BarState = { approval_viewer_role: "any_visible" };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.predicates?.approval_request?.viewer_role).toBe("any_visible");
  });

  test("approval_status chip narrows approval predicate", () => {
    const state: BarState = { approval_status: ["pending", "approved"] };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.predicates?.approval_request?.status).toEqual(["pending", "approved"]);
  });

  test("approval_entity_type chip narrows approval predicate", () => {
    const state: BarState = { approval_entity_type: ["deadline"] };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.predicates?.approval_request?.entity_types).toEqual(["deadline"]);
  });

  test("project_event_kind chip narrows project_event predicate", () => {
    const state: BarState = { project_event_kind: ["deadline_created"] };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.predicates?.project_event?.event_types).toEqual(["deadline_created"]);
  });

  test("time chip overrides base horizon", () => {
    const state: BarState = { time: { horizon: "past_7d" } };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.time.horizon).toBe("past_7d");
    expect(eff.filter.time.field).toBe("auto"); // preserved from base
  });

  test("personal_only chip flips scope flag", () => {
    const state: BarState = { personal_only: true };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.scope.personal_only).toBe(true);
  });

  test("multiple chips combine into the same effective spec", () => {
    const state: BarState = {
      time: { horizon: "past_7d" },
      deadline_status: ["pending"],
      appointment_type: ["hearing"],
      approval_status: ["pending"],
      project_event_kind: ["deadline_created"],
    };
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, state);
    expect(eff.filter.time.horizon).toBe("past_7d");
    expect(eff.filter.predicates?.deadline?.status).toEqual(["pending"]);
    expect(eff.filter.predicates?.appointment?.appointment_types).toEqual(["hearing"]);
    expect(eff.filter.predicates?.approval_request?.status).toEqual(["pending"]);
    expect(eff.filter.predicates?.project_event?.event_types).toEqual(["deadline_created"]);
  });

  test("overlay does not mutate the caller's base filter", () => {
    const base: FilterSpec = JSON.parse(JSON.stringify(ANY_VIEW_FILTER));
    const state: BarState = { deadline_status: ["pending"], time: { horizon: "past_7d" } };
    computeEffective(base, ANY_VIEW_RENDER, state);
    // The bar deep-clones; the base must come back unchanged so a
    // second click doesn't compound the previous click's overlay.
    expect(base).toEqual(ANY_VIEW_FILTER);
  });

  test("inbox-only axes do not affect a /views/any spec (no inbox axis exposed)", () => {
    // /views/any's axes don't include unread_only or inbox_focus, so
    // those keys never appear in state. Verify that even if they did,
    // the bar's overlay doesn't silently mutate sources or predicates
    // in a way that would break a 4-source Custom View.
    const eff = computeEffective(ANY_VIEW_FILTER, ANY_VIEW_RENDER, {});
    expect(eff.filter.sources).toHaveLength(4);
    expect(eff.filter.unread_only ?? false).toBe(false);
  });
});
