/**
 * LS7/LS8-front — {@link AgentLimitBadge} presentation + its pure helpers
 * (ARCHITECTURE §21).
 *
 * Three layers:
 *  - the exported pure helpers `formatCountdown` / `formatResetTime` /
 *    `timeInputToEpochMs` (edge cases: 0, negative, sub-minute, multi-minute,
 *    midnight, seconds-stripped, malformed/empty time input);
 *  - the rendered badge across its states (limité jusqu'à HH:MM / "limité"
 *    without a time / the "heure inconnue" resume-time form for a suspected limit
 *    without a reset), plus the "Annuler la reprise" button wiring when a resume
 *    is armed;
 *  - the human-net form (LS8): submitting a time arms a resume via onSetResumeAt.
 */

import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";

import {
  AgentLimitBadge,
  formatCountdown,
  formatResetTime,
  timeInputToEpochMs,
} from "./AgentLimitBadge";

describe("formatCountdown", () => {
  it("renders 0s for exactly zero", () => {
    expect(formatCountdown(0)).toBe("0s");
  });

  it("clamps a negative (past) deadline to 0s", () => {
    expect(formatCountdown(-5_000)).toBe("0s");
  });

  it("renders sub-minute durations as just seconds", () => {
    expect(formatCountdown(5_000)).toBe("5s");
    expect(formatCountdown(59_000)).toBe("59s");
  });

  it("rounds partial seconds up (ceil)", () => {
    expect(formatCountdown(4_200)).toBe("5s");
    expect(formatCountdown(1)).toBe("1s");
  });

  it("renders minutes + seconds past a minute", () => {
    expect(formatCountdown(60_000)).toBe("1m 0s");
    expect(formatCountdown(90_000)).toBe("1m 30s");
    expect(formatCountdown(125_000)).toBe("2m 5s");
  });
});

describe("formatResetTime", () => {
  it("produces an HH:MM wall-clock label (no seconds component)", () => {
    // Same minute, +30s apart → identical label (seconds are not shown).
    const base = new Date(2026, 0, 15, 9, 5, 0).getTime();
    const plus30s = new Date(2026, 0, 15, 9, 5, 30).getTime();
    expect(formatResetTime(base)).toBe(formatResetTime(plus30s));
    // Matches the documented HH:MM contract (delegates to toLocaleTimeString).
    expect(formatResetTime(base)).toBe(
      new Date(base).toLocaleTimeString([], {
        hour: "2-digit",
        minute: "2-digit",
      }),
    );
  });

  it("renders midnight as a stable two-digit label", () => {
    const midnight = new Date(2026, 0, 15, 0, 0, 0).getTime();
    const label = formatResetTime(midnight);
    expect(label).toBe(
      new Date(midnight).toLocaleTimeString([], {
        hour: "2-digit",
        minute: "2-digit",
      }),
    );
    // Minutes are "00" at midnight regardless of 12h/24h locale.
    expect(label).toContain("00");
  });
});

describe("timeInputToEpochMs", () => {
  it("maps HH:MM to the same calendar day as `now`", () => {
    const now = new Date(2026, 0, 15, 9, 0, 0).getTime();
    const ms = timeInputToEpochMs("14:30", now);
    expect(ms).not.toBeNull();
    const d = new Date(ms!);
    expect(d.getFullYear()).toBe(2026);
    expect(d.getMonth()).toBe(0);
    expect(d.getDate()).toBe(15);
    expect(d.getHours()).toBe(14);
    expect(d.getMinutes()).toBe(30);
    expect(d.getSeconds()).toBe(0);
    expect(d.getMilliseconds()).toBe(0);
  });

  it("returns a past instant unchanged (backend clamps to now)", () => {
    const now = new Date(2026, 0, 15, 18, 0, 0).getTime();
    const ms = timeInputToEpochMs("08:00", now);
    expect(ms).not.toBeNull();
    expect(ms!).toBeLessThan(now);
  });

  it("returns null for empty or malformed input", () => {
    const now = Date.now();
    expect(timeInputToEpochMs("", now)).toBeNull();
    expect(timeInputToEpochMs("nope", now)).toBeNull();
    expect(timeInputToEpochMs("25:00", now)).toBeNull();
    expect(timeInputToEpochMs("12:60", now)).toBeNull();
  });
});

describe("AgentLimitBadge (render)", () => {
  const noop = () => {};

  it("shows 'limité jusqu'à HH:MM' when the reset time is known", () => {
    const at = new Date(2026, 0, 15, 14, 30, 0).getTime();
    render(
      <AgentLimitBadge
        state={{ limitedUntil: at }}
        onCancelResume={noop}
        onSetResumeAt={noop}
      />,
    );
    const expected = `limité jusqu'à ${formatResetTime(at)}`;
    expect(screen.getByText(expected)).toBeTruthy();
    // No countdown / cancel button without an armed resume.
    expect(screen.queryByLabelText("cancel resume")).toBeNull();
  });

  it("shows plain 'limité' when no reset time is known", () => {
    render(
      <AgentLimitBadge state={{}} onCancelResume={noop} onSetResumeAt={noop} />,
    );
    expect(screen.getByText("limité")).toBeTruthy();
  });

  it("shows the 'heure inconnue' resume-time form for a suspected limit without a time", () => {
    render(
      <AgentLimitBadge
        state={{ suspected: true }}
        onCancelResume={noop}
        onSetResumeAt={noop}
      />,
    );
    expect(screen.getByText("limité")).toBeTruthy();
    expect(screen.getByText(/heure inconnue/)).toBeTruthy();
    // The human-net form: a resume-time input + a "schedule resume" button.
    expect(screen.getByLabelText("resume time")).toBeTruthy();
    expect(screen.getByLabelText("schedule resume")).toBeTruthy();
  });

  it("does NOT show the resume-time form when a suspected limit has a time", () => {
    const at = new Date(2026, 0, 15, 14, 30, 0).getTime();
    render(
      <AgentLimitBadge
        state={{ suspected: true, limitedUntil: at }}
        onCancelResume={noop}
        onSetResumeAt={noop}
      />,
    );
    expect(screen.getByText(`limité jusqu'à ${formatResetTime(at)}`)).toBeTruthy();
    expect(screen.queryByText(/heure inconnue/)).toBeNull();
    expect(screen.queryByLabelText("resume time")).toBeNull();
  });

  it("submitting the resume-time form arms a resume at the chosen epoch-ms", () => {
    const onSetResumeAt = vi.fn();
    render(
      <AgentLimitBadge
        state={{ suspected: true }}
        onCancelResume={noop}
        onSetResumeAt={onSetResumeAt}
      />,
    );
    fireEvent.change(screen.getByLabelText("resume time"), {
      target: { value: "23:45" },
    });
    fireEvent.click(screen.getByLabelText("schedule resume"));
    expect(onSetResumeAt).toHaveBeenCalledTimes(1);
    const ms = onSetResumeAt.mock.calls[0][0] as number;
    const d = new Date(ms);
    expect(d.getHours()).toBe(23);
    expect(d.getMinutes()).toBe(45);
  });

  it("does not arm a resume when the time input is empty (button disabled)", () => {
    const onSetResumeAt = vi.fn();
    render(
      <AgentLimitBadge
        state={{ suspected: true }}
        onCancelResume={noop}
        onSetResumeAt={onSetResumeAt}
      />,
    );
    expect(
      (screen.getByLabelText("schedule resume") as HTMLButtonElement).disabled,
    ).toBe(true);
    expect(onSetResumeAt).not.toHaveBeenCalled();
  });

  it("renders the countdown + calls onCancelResume when a resume is armed", () => {
    const onCancelResume = vi.fn();
    const fireAt = Date.now() + 90_000;
    render(
      <AgentLimitBadge
        state={{ limitedUntil: Date.now(), resumeFireAt: fireAt }}
        onCancelResume={onCancelResume}
        onSetResumeAt={noop}
      />,
    );
    // Countdown label present (≈ "reprise dans 1m 30s").
    expect(screen.getByLabelText("resume countdown").textContent).toMatch(
      /reprise dans/,
    );

    const btn = screen.getByLabelText("cancel resume");
    fireEvent.click(btn);
    expect(onCancelResume).toHaveBeenCalledTimes(1);
  });

  it("disables the cancel button while busy", () => {
    render(
      <AgentLimitBadge
        state={{ resumeFireAt: Date.now() + 10_000 }}
        onCancelResume={noop}
        onSetResumeAt={noop}
        busy
      />,
    );
    expect(
      (screen.getByLabelText("cancel resume") as HTMLButtonElement).disabled,
    ).toBe(true);
  });
});
