import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { GameInterpreter } from "../GameInterpreter";

// js-interpreter executa código real — usamos mock para controlar o comportamento de forma determinista
vi.mock("js-interpreter", () => {
  // Cada teste pode substituir o comportamento de mockStep através de mockStepFn
  const MockInterpreter = vi.fn(function (code, setup) {
    if (setup) setup(this, {});
    this.ast = { body: [{}] }; // AST não vazia por defeito
    this.step = mockStepFn;
    this.nativeToPseudo = vi.fn((v) => v);
    this.getValueFromScope = vi.fn(() => null);
    this.queueFunction = vi.fn();
  });
  return { default: MockInterpreter };
});

// Função step mutável — os testes podem substituí-la
let mockStepFn = vi.fn(() => false); // false = termina imediatamente

beforeEach(() => {
  mockStepFn = vi.fn(() => false); // repõe para "termina imediatamente"
  vi.useFakeTimers();
});

afterEach(() => {
  vi.useRealTimers();
});

// ── construtor ───────────────────────────────────────────────────────────────
describe("construtor", () => {
  it("usa stepDelay padrão de 20", () => {
    const gi = new GameInterpreter();
    expect(gi.config.stepDelay).toBe(20);
  });

  it("combina configuração personalizada", () => {
    const gi = new GameInterpreter({ stepDelay: 50 });
    expect(gi.config.stepDelay).toBe(50);
  });

  it("inicia com isRunning=false", () => {
    expect(new GameInterpreter().isRunning).toBe(false);
  });

  it("getter running reflete isRunning", () => {
    const gi = new GameInterpreter();
    expect(gi.running).toBe(false);
    gi.isRunning = true;
    expect(gi.running).toBe(true);
  });
});

// ── stop / stopInternal / setPause ───────────────────────────────────────────
describe("stop()", () => {
  it("define shouldStop e stoppedByUser como true", () => {
    const gi = new GameInterpreter();
    gi.stop();
    expect(gi.shouldStop).toBe(true);
    expect(gi.stoppedByUser).toBe(true);
  });
});

describe("stopInternal()", () => {
  it("define shouldStop=true mas stoppedByUser=false", () => {
    const gi = new GameInterpreter();
    gi.stopInternal();
    expect(gi.shouldStop).toBe(true);
    expect(gi.stoppedByUser).toBe(false);
  });
});

describe("setPause()", () => {
  it("atualiza o valor de pause", () => {
    const gi = new GameInterpreter();
    gi.setPause(100);
    expect(gi.pause).toBe(100);
  });
});

// ── executeCode — caminhos básicos ───────────────────────────────────────────
describe("executeCode", () => {
  it("devolve 'completed' quando o interpretador termina imediatamente", async () => {
    mockStepFn = vi.fn(() => false); // step devolve false = terminado
    const gi = new GameInterpreter();
    const p = gi.executeCode("x=1;", vi.fn());
    vi.runAllTimers();
    expect(await p).toBe("completed");
  });

  it("devolve 'completed' para código vazio (AST body vazio)", async () => {
    const Interpreter = (await import("js-interpreter")).default;
    Interpreter.mockImplementationOnce(function (code, setup) {
      this.ast = { body: [] }; // body vazio
      this.step = vi.fn(() => false);
    });
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const gi = new GameInterpreter();
    const result = await gi.executeCode("", vi.fn());
    expect(result).toBe("completed");
    spy.mockRestore();
  });

  it("define isRunning=false após a conclusão", async () => {
    const gi = new GameInterpreter();
    const p = gi.executeCode("x=1;", vi.fn());
    vi.runAllTimers();
    await p;
    expect(gi.isRunning).toBe(false);
  });

  it("lança exceção e limpa quando o construtor do Interpreter lança", async () => {
    const Interpreter = (await import("js-interpreter")).default;
    Interpreter.mockImplementationOnce(() => {
      throw new Error("parse error");
    });
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    const gi = new GameInterpreter();
    await expect(gi.executeCode("bad code", vi.fn())).rejects.toThrow(
      "parse error",
    );
    expect(gi.isRunning).toBe(false);
    spy.mockRestore();
  });

  it("aguarda se já em execução e depois executa", async () => {
    // A primeira execução usa um step que demora mais ciclos
    let callCount = 0;
    mockStepFn = vi.fn(() => {
      callCount++;
      return callCount < 2; // executa 2 steps e termina
    });

    const gi = new GameInterpreter();
    gi.isRunning = true; // simula já em execução

    // Inicia a segunda chamada — deve aguardar
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    gi.isRunning = false; // permite prosseguir após a verificação
    const p = gi.executeCode("x=1;", vi.fn());
    vi.runAllTimers();
    const result = await p;
    expect(result).toBeDefined();
    spy.mockRestore();
  });
});

// ── executeCode — caminhos de paragem ────────────────────────────────────────
describe("executeCode — caminhos de paragem", () => {
  it("devolve 'stopped_by_user' quando stop() é chamado durante a execução", async () => {
    const gi = new GameInterpreter();
    let stepped = false;
    mockStepFn = vi.fn(() => {
      if (!stepped) {
        stepped = true;
        gi.stop(); // aciona paragem no primeiro step
        return true; // simula que há mais código para executar
      }
      return false;
    });
    const p = gi.executeCode("x=1;", vi.fn());
    vi.runAllTimers();
    expect(await p).toBe("stopped_by_user");
  });

  it("devolve 'stopped' quando stopInternal() é chamado durante a execução", async () => {
    const gi = new GameInterpreter();
    let stepped = false;
    mockStepFn = vi.fn(() => {
      if (!stepped) {
        stepped = true;
        gi.stopInternal();
        return true;
      }
      return false;
    });
    const p = gi.executeCode("x=1;", vi.fn());
    vi.runAllTimers();
    expect(await p).toBe("stopped");
  });

  it("rejeita quando step() lança exceção", async () => {
    mockStepFn = vi.fn(() => {
      throw new Error("step error");
    });
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    const gi = new GameInterpreter();
    await expect(gi.executeCode("x=1;", vi.fn())).rejects.toThrow("step error");
    spy.mockRestore();
  });
});

// ── executeFunction ───────────────────────────────────────────────────────────
describe("executeFunction", () => {
  it("devolve imediatamente quando o interpretador é null", async () => {
    const gi = new GameInterpreter();
    // interpretador é null por defeito
    const result = await gi.executeFunction("myFunc");
    expect(result).toBeUndefined();
  });

  it("emite aviso quando a função não é encontrada no scope", async () => {
    const gi = new GameInterpreter();
    // Define o interpretador manualmente sem globalScope
    gi.interpreter = {
      nativeToPseudo: vi.fn((v) => v),
      getValueFromScope: vi.fn(() => null), // devolve null — não é uma função
      queueFunction: vi.fn(),
    };
    gi.globalScope = {}; // define para que a verificação de null passe
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    await gi.executeFunction("missing");
    expect(spy).toHaveBeenCalled();
    spy.mockRestore();
  });
});

// ── executeFunction — bloco catch ─────────────────────────────────────────────
describe("executeFunction — bloco catch", () => {
  it("regista console.error quando nativeToPseudo lança exceção", async () => {
    const gi = new GameInterpreter();
    gi.interpreter = {
      nativeToPseudo: vi.fn(() => {
        throw new Error("pseudo error");
      }),
      getValueFromScope: vi.fn(() => null),
      queueFunction: vi.fn(),
    };
    gi.globalScope = {};
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    await gi.executeFunction("myFunc", 42);
    expect(spy).toHaveBeenCalled();
    spy.mockRestore();
  });
});

// ── _runSteps — mecanismo de divisão em blocos ────────────────────────────────
describe("_runSteps — mecanismo de divisão em blocos", () => {
  it("agenda o próximo bloco após 1000 steps e resolve 'completed'", async () => {
    let callCount = 0;
    // Devolve true para 1001 chamadas (atingindo o limite de 1000 ciclos) e depois false
    mockStepFn = vi.fn(() => {
      callCount++;
      return callCount <= 1001; // primeiros 1001 → true, 1002º → false
    });

    const gi = new GameInterpreter();
    const p = gi.executeCode("loop", vi.fn());

    // O primeiro bloco executa 1001 steps, atinge o limite de ciclos e agenda o próximo bloco
    // Avança os timers para acionar o callback de executeChunk
    vi.runAllTimers();

    expect(await p).toBe("completed");
  });
});

// ── executeFunction — caminho de sucesso (funcObject.isFunc = true) ───────────
describe("executeFunction — caminho de sucesso", () => {
  it("chama queueFunction e executa steps quando a função é encontrada", async () => {
    const gi = new GameInterpreter();
    const mockQueueFunction = vi.fn();
    gi.interpreter = {
      nativeToPseudo: vi.fn((v) => v),
      getValueFromScope: vi.fn(() => ({ isFunc: true })),
      queueFunction: mockQueueFunction,
      step: vi.fn(() => false), // step termina imediatamente
    };
    gi.globalScope = {};
    await gi.executeFunction("myFunc", 1, 2);
    expect(mockQueueFunction).toHaveBeenCalled();
  });
});

// ── executeCode — sanitização ─────────────────────────────────────────────────
describe("executeCode — sanitização", () => {
  it("rejeita com CodeSanitizationError para while(true) infinito sem break", async () => {
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const gi = new GameInterpreter();
    await expect(
      gi.executeCode("while(true) { doSomething(); }", vi.fn()),
    ).rejects.toMatchObject({
      name: "CodeSanitizationError",
      rule: "infinite_while",
    });
    expect(spy).toHaveBeenCalledWith(
      "[GameInterpreter] Código rejeitado pelo sanitizador:",
      "infinite_while",
      expect.any(String),
    );
    spy.mockRestore();
  });

  it("rejeita com CodeSanitizationError quando o código excede max_length", async () => {
    const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const gi = new GameInterpreter();
    await expect(
      gi.executeCode("a".repeat(50_001), vi.fn()),
    ).rejects.toMatchObject({ name: "CodeSanitizationError", rule: "max_length" });
    spy.mockRestore();
  });

  it("não chama console.error para erros de sanitização (apenas console.warn)", async () => {
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    const gi = new GameInterpreter();
    await expect(
      gi.executeCode("while(true) { doSomething(); }", vi.fn()),
    ).rejects.toMatchObject({ name: "CodeSanitizationError" });
    expect(errSpy).not.toHaveBeenCalled();
    warnSpy.mockRestore();
    errSpy.mockRestore();
  });

  it("permite código válido passar pela sanitização", async () => {
    mockStepFn = vi.fn(() => false);
    const gi = new GameInterpreter();
    const p = gi.executeCode("var x = 1;", vi.fn());
    vi.runAllTimers();
    expect(await p).toBe("completed");
  });
});
