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

const uploadMock = vi.fn(
  async (_args: { conversationId: string; file: File | Blob; filename?: string; notaDeVoz?: boolean }) => ({
    storage_path: "org/conv/out-a.ogg",
    media_mime: "audio/ogg",
    media_size_bytes: 5,
    kind: "audio" as const,
  }),
);
const sendMock = vi.fn();
vi.mock("@/hooks/inbox/useUploadMedia", () => ({
  useUploadMedia: () => ({ mutateAsync: uploadMock, isPending: false }),
}));
vi.mock("@/hooks/inbox/useSendMessage", () => ({
  useSendMessage: () => ({ mutate: sendMock, isPending: false }),
}));

import { AudioRecorder } from "@/components/inbox/composer/AudioRecorder";

class FakeRecorder {
  static instances: FakeRecorder[] = [];
  static isTypeSupported: (mime: string) => boolean = () => true;
  ondataavailable: ((e: { data: Blob }) => void) | null = null;
  onstop: (() => void) | null = null;
  state = "inactive";
  mimeType = "audio/webm;codecs=opus";
  constructor(_stream: unknown, opts?: { mimeType?: string }) {
    if (opts?.mimeType) this.mimeType = opts.mimeType;
    FakeRecorder.instances.push(this);
  }
  start() {
    this.state = "recording";
  }
  stop() {
    this.state = "inactive";
    this.ondataavailable?.({ data: new Blob([new Uint8Array([1, 2])], { type: this.mimeType }) });
    this.onstop?.();
  }
}

let trackStopMock: ReturnType<typeof vi.fn>;

describe("AudioRecorder", () => {
  beforeEach(() => {
    uploadMock.mockClear();
    sendMock.mockClear();
    FakeRecorder.instances = [];
    trackStopMock = vi.fn();
    vi.stubGlobal("MediaRecorder", FakeRecorder as unknown as typeof MediaRecorder);
    Object.defineProperty(navigator, "mediaDevices", {
      configurable: true,
      value: {
        getUserMedia: vi.fn(async () => ({ getTracks: () => [{ stop: trackStopMock }] })),
      },
    });
  });

  it("stops a late microphone permission result after unmount without uploading", async () => {
    let resolve!: (value: MediaStream) => void;
    vi.mocked(navigator.mediaDevices.getUserMedia).mockReturnValue(
      new Promise<MediaStream>((r) => {
        resolve = r;
      }),
    );
    const view = render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    view.unmount();
    await act(async () => {
      resolve({ getTracks: () => [{ stop: trackStopMock }] } as unknown as MediaStream);
    });
    expect(trackStopMock).toHaveBeenCalledOnce();
    expect(FakeRecorder.instances).toHaveLength(0);
    expect(uploadMock).not.toHaveBeenCalled();
    expect(sendMock).not.toHaveBeenCalled();
  });

  it("mic inicia gravação e mostra timer + cancelar", async () => {
    render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    expect(await screen.findByRole("button", { name: /cancelar gravação/i })).toBeInTheDocument();
    expect(screen.getByText(/0:0\d/)).toBeInTheDocument();
  });

  it("enviar para a gravação, sobe o blob com mime real e envia type audio", async () => {
    render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    await screen.findByRole("button", { name: /enviar áudio/i });
    await act(async () => {
      fireEvent.click(screen.getByRole("button", { name: /enviar áudio/i }));
    });
    await waitFor(() => expect(uploadMock).toHaveBeenCalled());
    const arg = uploadMock.mock.calls[0]![0] as { file: Blob };
    expect(arg.file.type).toContain("audio/");
    await waitFor(() =>
      expect(sendMock).toHaveBeenCalledWith(
        expect.objectContaining({ type: "audio", media_storage_path: "org/conv/out-a.ogg" }),
        expect.anything(),
      ),
    );
  });

  it("cancelar descarta sem upload", async () => {
    render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    fireEvent.click(await screen.findByRole("button", { name: /cancelar gravação/i }));
    expect(uploadMock).not.toHaveBeenCalled();
    expect(screen.getByRole("button", { name: /gravar áudio/i })).toBeInTheDocument();
  });

  it("desmontar durante a gravação libera o microfone", async () => {
    const { unmount } = render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    await screen.findByRole("button", { name: /cancelar gravação/i });
    expect(trackStopMock).not.toHaveBeenCalled();
    unmount();
    expect(trackStopMock).toHaveBeenCalled();
  });

  it("clicar em enviar duas vezes rápido não quebra e sobe uma vez só", async () => {
    render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    const sendBtn = await screen.findByRole("button", { name: /enviar áudio/i });
    await act(async () => {
      expect(() => {
        fireEvent.click(sendBtn);
        fireEvent.click(sendBtn);
      }).not.toThrow();
    });
    await waitFor(() => expect(uploadMock).toHaveBeenCalledTimes(1));
  });

  /**
   * F2: o Safari do iPhone (antes do 18.4) não grava webm nem ogg — o
   * `MediaRecorder` cai no padrão `audio/mp4`. Subir isso como `ptt.webm`
   * mentia o formato; e sem a marca `notaDeVoz` o servidor não converteria o
   * AAC em opus, e o canal oficial receberia um áudio que não é nota de voz.
   */
  it("iPhone: mp4 sobe como ptt.m4a e marcado como nota de voz", async () => {
    vi.stubGlobal(
      "MediaRecorder",
      class extends FakeRecorder {
        static isTypeSupported = () => false;
        mimeType = "audio/mp4";
      } as unknown as typeof MediaRecorder,
    );
    render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    const enviar = await screen.findByRole("button", { name: /enviar áudio/i });
    await act(async () => {
      fireEvent.click(enviar);
    });
    await waitFor(() => expect(uploadMock).toHaveBeenCalled());
    expect(uploadMock.mock.calls[0]![0]).toMatchObject({ filename: "ptt.m4a", notaDeVoz: true });
  });

  it("Chrome: webm continua ptt.webm, também marcado como nota de voz", async () => {
    vi.stubGlobal(
      "MediaRecorder",
      class extends FakeRecorder {
        static isTypeSupported = (m: string) => m.includes("webm");
      } as unknown as typeof MediaRecorder,
    );
    render(<AudioRecorder conversationId="conv-1" />);
    fireEvent.click(screen.getByRole("button", { name: /gravar áudio/i }));
    const enviar = await screen.findByRole("button", { name: /enviar áudio/i });
    await act(async () => {
      fireEvent.click(enviar);
    });
    await waitFor(() => expect(uploadMock).toHaveBeenCalled());
    expect(uploadMock.mock.calls[0]![0]).toMatchObject({ filename: "ptt.webm", notaDeVoz: true });
  });
});
