import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { GameStateProvider, useGameState, GAME_STATES } from "../GameStateContext";

// ── mock gameEventBus ─────────────────────────────────────────────────────────

const { mockStopExecution } = vi.hoisted(() => ({
  mockStopExecution: vi.fn(),
}));
vi.mock("../../utils/gameEvents", () => ({
  gameEventBus: { stopExecution: mockStopExecution },
}));

// ── helpers ───────────────────────────────────────────────────────────────────

const GAME_CONFIG = {
  gameId: "test-game",
  fases: [{ id: 1 }, { id: 2 }, { id: 3 }],
};

function Consumer() {
  const ctx = useGameState();
  return (
    <div>
      <span data-testid="state">{ctx.executionState}</span>
      <span data-testid="phase">{ctx.currentPhase}</span>
      <span data-testid="completed">{JSON.stringify(ctx.completedPhases)}</span>
      <span data-testid="code">{typeof ctx.generatedCode === "object"
        ? JSON.stringify(ctx.generatedCode)
        : ctx.generatedCode}
      </span>
      <span data-testid="blockCount">{ctx.currentBlockCount}</span>
      <span data-testid="editorType">{ctx.editorType}</span>
      <span data-testid="codeContent">{ctx.codeEditorContent}</span>
      <span data-testid="failMsg">{ctx.failureMessage}</span>
      <span data-testid="debug">{String(ctx.isDebugMode)}</span>

      <button onClick={() => ctx.execute()}>execute</button>
      <button onClick={() => ctx.finalizeWithSuccess()}>success</button>
      <button onClick={() => ctx.finalizeWithFailure()}>failure</button>
      <button onClick={() => ctx.restart()}>restart</button>
      <button onClick={() => ctx.stop()}>stop</button>
      <button onClick={() => ctx.resetProgress()}>reset</button>
      <button onClick={() => ctx.setCurrentPhase(2)}>phase2</button>
      <button onClick={() => ctx.setEditorType("code")}>setCode</button>
      <button onClick={() => ctx.setFailureMessage("erro!")}>setFail</button>
      <button onClick={() => ctx.onWorkspaceChange(5)}>wsChange</button>
      <button onClick={() => ctx.onCodeEditorChange("x = 1")}>codeChange</button>
      <button onClick={() => ctx.onCodeEditorChange("")}>codeChangeClear</button>
      <button onClick={() => ctx.setOnWorkspaceChange((n) => {})}>setWsCb</button>
      <button onClick={() => {
        ctx.registerExecutionFunction(() => ({ codigo: "js code", workspace: {} }));
      }}>regBlockly</button>
      <button onClick={() => {
        ctx.registerCodeEditorFunction(() => "my code");
      }}>regCode</button>
      <button onClick={() => {
        ctx.registerExecutionFunction(null);
        ctx.execute();
      }}>execNoWs</button>
    </div>
  );
}

function renderProvider(gameConfig = GAME_CONFIG) {
  return render(
    <GameStateProvider gameConfig={gameConfig}>
      <Consumer />
    </GameStateProvider>
  );
}

// ── useGameState outside provider ─────────────────────────────────────────────

describe("useGameState", () => {
  it("throws quando usado fora de GameStateProvider", () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    expect(() => render(<Consumer />)).toThrow(
      "useGameState deve ser usado dentro de GameStateProvider"
    );
    spy.mockRestore();
  });
});

// ── initial state ──────────────────────────────────────────────────────────────

describe("GameStateProvider — estado inicial", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("executionState inicial é PARADO", () => {
    renderProvider();
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.PARADO);
  });

  it("currentPhase inicial é 1", () => {
    renderProvider();
    expect(screen.getByTestId("phase").textContent).toBe("1");
  });

  it("completedPhases inicial é []", () => {
    renderProvider();
    expect(screen.getByTestId("completed").textContent).toBe("[]");
  });

  it("editorType padrão é blockly", () => {
    renderProvider();
    expect(screen.getByTestId("editorType").textContent).toBe("blockly");
  });

  it("GAME_STATES exporta os 4 valores esperados", () => {
    expect(GAME_STATES.PARADO).toBe("parado");
    expect(GAME_STATES.EXECUTANDO).toBe("executando");
    expect(GAME_STATES.SUCESSO).toBe("sucesso");
    expect(GAME_STATES.FALHA).toBe("falha");
  });
});

// ── isDebugMode via URL ────────────────────────────────────────────────────────

describe("GameStateProvider — isDebugMode", () => {
  afterEach(() => { localStorage.clear(); });

  it("isDebugMode=true quando URL tem debug=true", () => {
    delete window.location;
    window.location = { search: "?debug=true" };
    renderProvider();
    expect(screen.getByTestId("debug").textContent).toBe("true");
  });

  it("isDebugMode=false quando sem parâmetro debug", () => {
    delete window.location;
    window.location = { search: "" };
    renderProvider();
    expect(screen.getByTestId("debug").textContent).toBe("false");
  });
});

// ── execute — blockly ──────────────────────────────────────────────────────────

describe("execute — modo blockly", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("muda para EXECUTANDO quando workspace registrada retorna código", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("regBlockly"));
    await user.click(screen.getByText("execute"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.EXECUTANDO);
  });

  it("loga erro quando nenhuma função blockly foi registrada", async () => {
    const user = userEvent.setup();
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    renderProvider();
    await user.click(screen.getByText("execute"));
    expect(spy).toHaveBeenCalledWith(
      expect.stringContaining("BlocklyEditor")
    );
    spy.mockRestore();
  });
});

// ── execute — code editor ──────────────────────────────────────────────────────

describe("execute — modo code editor", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("muda para EXECUTANDO com editorType=code e função registrada", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("setCode"));
    await user.click(screen.getByText("regCode"));
    await user.click(screen.getByText("execute"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.EXECUTANDO);
  });

  it("não muda estado quando editorType=code mas nenhuma função registrada", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("setCode"));
    await user.click(screen.getByText("execute"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.PARADO);
  });
});

// ── finalizeWithSuccess ────────────────────────────────────────────────────────

describe("finalizeWithSuccess", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("muda estado para SUCESSO", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("success"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.SUCESSO);
  });

  it("adiciona fase atual às completedPhases", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("success"));
    await waitFor(() => {
      expect(screen.getByTestId("completed").textContent).toContain("1");
    });
  });

  it("não duplica a fase se já completada", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("success"));
    await user.click(screen.getByText("success"));
    await waitFor(() => {
      const completed = JSON.parse(screen.getByTestId("completed").textContent);
      expect(completed.filter((x) => x === 1)).toHaveLength(1);
    });
  });
});

// ── finalizeWithFailure ────────────────────────────────────────────────────────

describe("finalizeWithFailure", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("muda estado para FALHA", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("failure"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.FALHA);
  });
});

// ── restart ────────────────────────────────────────────────────────────────────

describe("restart", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("volta para PARADO e limpa generatedCode", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("regBlockly"));
    await user.click(screen.getByText("execute"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.EXECUTANDO);
    await user.click(screen.getByText("restart"));
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.PARADO);
    expect(screen.getByTestId("code").textContent.trim()).toBe("");
  });
});

// ── stop ───────────────────────────────────────────────────────────────────────

describe("stop", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
    mockStopExecution.mockClear();
  });

  it("chama gameEventBus.stopExecution() e volta para PARADO", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("stop"));
    expect(mockStopExecution).toHaveBeenCalled();
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.PARADO);
  });
});

// ── resetProgress ──────────────────────────────────────────────────────────────

describe("resetProgress", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("limpa progresso e volta para fase 1", async () => {
    const user = userEvent.setup();
    localStorage.setItem("test-game-completed-phases", JSON.stringify([1]));
    renderProvider();
    await waitFor(() =>
      expect(screen.getByTestId("completed").textContent).toContain("1")
    );
    await user.click(screen.getByText("reset"));
    await waitFor(() => {
      expect(screen.getByTestId("completed").textContent).toBe("[]");
      expect(screen.getByTestId("phase").textContent).toBe("1");
    });
  });
});

// ── changePhase (setCurrentPhase alias) ───────────────────────────────────────

describe("changePhase via setCurrentPhase", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("muda fase, reseta estado e limpa código e contador", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("regBlockly"));
    await user.click(screen.getByText("execute"));
    await user.click(screen.getByText("wsChange")); // blockCount=5
    await user.click(screen.getByText("phase2"));
    expect(screen.getByTestId("phase").textContent).toBe("2");
    expect(screen.getByTestId("state").textContent).toBe(GAME_STATES.PARADO);
    expect(screen.getByTestId("code").textContent.trim()).toBe("");
    expect(screen.getByTestId("blockCount").textContent).toBe("0");
  });
});

// ── onWorkspaceChange ──────────────────────────────────────────────────────────

describe("onWorkspaceChange", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("atualiza currentBlockCount", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("wsChange"));
    expect(screen.getByTestId("blockCount").textContent).toBe("5");
  });

  it("chama o callback externo se registrado via setOnWorkspaceChange", async () => {
    const user = userEvent.setup();
    const externalCb = vi.fn();

    function ConsumerWithCb() {
      const ctx = useGameState();
      return (
        <div>
          <span data-testid="count">{ctx.currentBlockCount}</span>
          <button onClick={() => ctx.setOnWorkspaceChange(externalCb)}>setCb</button>
          <button onClick={() => ctx.onWorkspaceChange(7)}>trigger</button>
        </div>
      );
    }
    render(
      <GameStateProvider gameConfig={GAME_CONFIG}>
        <ConsumerWithCb />
      </GameStateProvider>
    );
    await user.click(screen.getByText("setCb"));
    await user.click(screen.getByText("trigger"));
    expect(externalCb).toHaveBeenCalledWith(7);
  });
});

// ── onCodeEditorChange ─────────────────────────────────────────────────────────

describe("onCodeEditorChange", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("atualiza codeEditorContent e blockCount=1 quando há conteúdo", async () => {
    const user = userEvent.setup();
    renderProvider();
    // Precisamos estar em modo code com função registrada para o effect não sobrescrever
    await user.click(screen.getByText("setCode"));
    await user.click(screen.getByText("regCode"));
    await user.click(screen.getByText("codeChange"));
    await waitFor(() => {
      expect(screen.getByTestId("codeContent").textContent).toBe("x = 1");
      expect(screen.getByTestId("blockCount").textContent).toBe("1");
    });
  });

  it("blockCount=0 quando conteúdo está vazio", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("codeChange"));
    await user.click(screen.getByText("codeChangeClear"));
    expect(screen.getByTestId("blockCount").textContent).toBe("0");
  });
});

// ── setFailureMessage ──────────────────────────────────────────────────────────

describe("setFailureMessage", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("atualiza failureMessage", async () => {
    const user = userEvent.setup();
    renderProvider();
    await user.click(screen.getByText("setFail"));
    expect(screen.getByTestId("failMsg").textContent).toBe("erro!");
  });
});

// ── editorType effect ──────────────────────────────────────────────────────────

describe("editorType effect", () => {
  beforeEach(() => {
    localStorage.clear();
    delete window.location;
    window.location = { search: "" };
  });

  it("em modo code com função registrada, blockCount reflecte codeEditorContent vazio", async () => {
    const user = userEvent.setup();
    renderProvider();
    // Registra função e entra em modo code com conteúdo
    await user.click(screen.getByText("setCode"));
    await user.click(screen.getByText("regCode"));
    await user.click(screen.getByText("codeChange")); // codeEditorContent="x = 1" → blockCount=1
    // Limpa conteúdo → effect roda: codeEditorContent.trim()="" → blockCount=0
    await user.click(screen.getByText("codeChangeClear"));
    await waitFor(() => {
      expect(screen.getByTestId("blockCount").textContent).toBe("0");
    });
  });
});
