/**
 * @file UpdateNotifier.test.tsx
 * @description Pins the "Update now" self-apply flow added on top of the
 * existing manual-command modal: the button only renders when the checkout
 * situation is fast-forwardable, clicking it calls `api.updates.apply()`,
 * and a successful `applied: true` response flips the modal into a
 * "restarting" state that polls `api.updates.status()` until it resolves.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "i18next";
import { UpdateNotifier } from "../UpdateNotifier";
import type { UpdateStatusPayload } from "../../lib/types";

const statusMock = vi.fn();
const checkMock = vi.fn();
const applyMock = vi.fn();

vi.mock("../../lib/api", async (importOriginal) => {
  const actual = await importOriginal<Record<string, unknown>>();
  return {
    ...actual,
    api: {
      updates: {
        status: (...args: unknown[]) => statusMock(...args),
        check: (...args: unknown[]) => checkMock(...args),
        apply: (...args: unknown[]) => applyMock(...args),
      },
    },
  };
});

const BASE_STATUS: UpdateStatusPayload = {
  git_repo: true,
  update_available: true,
  repo_root: "/repo",
  remote_ref: "origin/main",
  canonical_remote: "origin",
  current_branch: "main",
  tracking_upstream: "origin/main",
  tracks_canonical: true,
  situation: "tracking_canonical",
  situation_note: null,
  local_sha: "aaa",
  remote_sha: "bbb",
  commits_behind: 2,
  manual_command: 'cd "/repo" && git pull --ff-only && npm run setup',
  message: "2 commit(s) on origin/main not in your checkout.",
};

beforeEach(() => {
  statusMock.mockReset().mockResolvedValue(BASE_STATUS);
  checkMock.mockReset().mockResolvedValue(BASE_STATUS);
  applyMock.mockReset();
  try {
    localStorage.clear();
  } catch {
    // Some CI environments stub a non-functional localStorage; the component
    // already tolerates that (see UpdateNotifier's own try/catch), so tests do too.
  }
});

describe("UpdateNotifier — Update now", () => {
  it("shows the Update now button for a fast-forwardable checkout and applies on click", async () => {
    applyMock.mockResolvedValue({ ...BASE_STATUS, applied: true, update_available: false });
    render(<UpdateNotifier />);

    const updateBtn = await screen.findByText(i18n.t("updates:updateNow"));
    await userEvent.click(updateBtn);

    await waitFor(() => expect(applyMock).toHaveBeenCalled());
    // Appears both in the status banner and the button label while restarting.
    const restartingNodes = await screen.findAllByText(i18n.t("updates:restarting"));
    expect(restartingNodes.length).toBeGreaterThan(0);
  });

  it("hides the Update now button for a non-fast-forwardable branch", async () => {
    statusMock.mockResolvedValue({
      ...BASE_STATUS,
      situation: "feature_branch",
      tracks_canonical: false,
    });
    render(<UpdateNotifier />);

    await screen.findByText(i18n.t("updates:title"));
    expect(screen.queryByText(i18n.t("updates:updateNow"))).not.toBeInTheDocument();
  });

  it("surfaces the decline reason instead of restarting on a 409", async () => {
    applyMock.mockResolvedValue({
      ...BASE_STATUS,
      applied: false,
      reason: "not_fast_forwardable",
    });
    render(<UpdateNotifier />);

    const updateBtn = await screen.findByText(i18n.t("updates:updateNow"));
    await userEvent.click(updateBtn);

    expect(await screen.findByText(i18n.t("updates:reasonNotFastForwardable"))).toBeInTheDocument();
    expect(screen.queryByText(i18n.t("updates:restarting"))).not.toBeInTheDocument();
  });
});
