import { Hono } from "hono";
import { describe, it, expect, vi, beforeEach } from "vitest";

/* ── Mocks ────────────────────────────────────────────────────────── */

const mockEnvManager = vi.hoisted(() => ({
  listEnvs: vi.fn().mockReturnValue([]),
  getEnv: vi.fn().mockReturnValue(null),
  createEnv: vi.fn(),
  updateEnv: vi.fn().mockReturnValue(null),
  updateBuildStatus: vi.fn(),
  deleteEnv: vi.fn().mockReturnValue(false),
}));

const mockContainerManager = vi.hoisted(() => ({
  containerManager: {
    checkDocker: vi.fn().mockReturnValue(true),
    buildImage: vi.fn().mockReturnValue("build log"),
    buildImageStreaming: vi.fn().mockResolvedValue({ success: true, log: "ok" }),
    imageExists: vi.fn().mockReturnValue(false),
  },
}));

const mockImagePullManager = vi.hoisted(() => ({
  imagePullManager: {
    getState: vi.fn().mockReturnValue({ status: "idle", log: [], error: null }),
    pull: vi.fn(),
  },
}));

const mockRedactUtils = vi.hoisted(() => ({
  redactEnvVariables: vi.fn().mockImplementation((vars: Record<string, string>) => {
    const redacted: Record<string, string> = {};
    for (const [k, v] of Object.entries(vars)) {
      redacted[k] = v ? "***" : "";
    }
    return redacted;
  }),
}));

vi.mock("../env-manager.js", () => mockEnvManager);
vi.mock("../container-manager.js", () => mockContainerManager);
vi.mock("../image-pull-manager.js", () => mockImagePullManager);
vi.mock("../redact-utils.js", () => mockRedactUtils);

/* ── App setup ────────────────────────────────────────────────────── */

import { registerEnvRoutes } from "./env-routes.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockEnvManager.listEnvs.mockReturnValue([]);
  mockEnvManager.getEnv.mockReturnValue(null);
  mockEnvManager.createEnv.mockReset();
  mockEnvManager.updateEnv.mockReturnValue(null);
  mockEnvManager.deleteEnv.mockReturnValue(false);
  mockEnvManager.updateBuildStatus.mockReset();
  mockContainerManager.containerManager.checkDocker.mockReturnValue(true);
  mockContainerManager.containerManager.buildImage.mockReturnValue("build log");
  mockContainerManager.containerManager.buildImageStreaming.mockResolvedValue({ success: true, log: "ok" });
  mockContainerManager.containerManager.imageExists.mockReturnValue(false);
  mockImagePullManager.imagePullManager.getState.mockReturnValue({ status: "idle", log: [], error: null });
  mockImagePullManager.imagePullManager.pull.mockReset();
  mockRedactUtils.redactEnvVariables.mockImplementation((vars: Record<string, string>) => {
    const redacted: Record<string, string> = {};
    for (const [k, v] of Object.entries(vars)) {
      redacted[k] = v ? "***" : "";
    }
    return redacted;
  });
  app = new Hono();
  registerEnvRoutes(app, { webDir: "/fake/web" });
});

/* ── Helpers ──────────────────────────────────────────────────────── */

function req(method: string, path: string, body?: unknown) {
  const init: RequestInit = { method, headers: {} };
  if (body) {
    (init.headers as Record<string, string>)["content-type"] = "application/json";
    init.body = JSON.stringify(body);
  }
  return app.request(path, init);
}

function makeEnv(overrides: Record<string, unknown> = {}) {
  return {
    slug: "test-env",
    name: "Test Env",
    variables: { API_KEY: "secret123" },
    dockerfile: "",
    imageTag: "",
    baseImage: "",
    buildStatus: "idle",
    createdAt: Date.now(),
    updatedAt: Date.now(),
    ...overrides,
  };
}

/* ── Tests ────────────────────────────────────────────────────────── */

describe("GET /envs", () => {
  it("returns empty list", async () => {
    const res = await req("GET", "/envs");
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual([]);
  });

  it("returns redacted envs", async () => {
    mockEnvManager.listEnvs.mockReturnValue([makeEnv()]);
    const res = await req("GET", "/envs");
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toHaveLength(1);
    expect(data[0].variables.API_KEY).toBe("***");
  });

  it("returns 500 on error", async () => {
    mockEnvManager.listEnvs.mockImplementation(() => { throw new Error("boom"); });
    const res = await req("GET", "/envs");
    expect(res.status).toBe(500);
  });
});

describe("GET /envs/:slug", () => {
  it("returns 404 for non-existent env", async () => {
    const res = await req("GET", "/envs/missing");
    expect(res.status).toBe(404);
  });

  it("returns redacted env", async () => {
    mockEnvManager.getEnv.mockReturnValue(makeEnv());
    const res = await req("GET", "/envs/test-env");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.slug).toBe("test-env");
    expect(body.variables.API_KEY).toBe("***");
  });
});

describe("POST /envs", () => {
  it("creates an env and returns 201", async () => {
    const created = makeEnv({ slug: "new-env" });
    mockEnvManager.createEnv.mockReturnValue(created);
    const res = await req("POST", "/envs", { name: "New Env" });
    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.slug).toBe("new-env");
  });

  it("returns 400 on missing name", async () => {
    const res = await req("POST", "/envs", {});
    expect(res.status).toBe(400);
  });

  it("returns 400 on store error", async () => {
    mockEnvManager.createEnv.mockImplementation(() => { throw new Error("Duplicate"); });
    const res = await req("POST", "/envs", { name: "Dup" });
    expect(res.status).toBe(400);
  });
});

describe("PUT /envs/:slug", () => {
  it("returns 404 for non-existent env", async () => {
    const res = await req("PUT", "/envs/missing", { name: "Updated" });
    expect(res.status).toBe(404);
  });

  it("updates and returns redacted env", async () => {
    const updated = makeEnv({ name: "Updated" });
    mockEnvManager.updateEnv.mockReturnValue(updated);
    const res = await req("PUT", "/envs/test-env", { name: "Updated" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.name).toBe("Updated");
  });

  it("returns 400 on store error", async () => {
    mockEnvManager.updateEnv.mockImplementation(() => { throw new Error("Bad"); });
    const res = await req("PUT", "/envs/test-env", { name: "Bad" });
    expect(res.status).toBe(400);
  });
});

describe("DELETE /envs/:slug", () => {
  it("returns 404 for non-existent env", async () => {
    const res = await req("DELETE", "/envs/missing");
    expect(res.status).toBe(404);
  });

  it("deletes env and returns ok", async () => {
    mockEnvManager.deleteEnv.mockReturnValue(true);
    const res = await req("DELETE", "/envs/test-env");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });
});

describe("POST /envs/:slug/build", () => {
  it("returns 404 for non-existent env", async () => {
    const res = await req("POST", "/envs/missing/build");
    expect(res.status).toBe(404);
  });

  it("returns 400 when no dockerfile configured", async () => {
    mockEnvManager.getEnv.mockReturnValue(makeEnv({ dockerfile: "" }));
    const res = await req("POST", "/envs/test-env/build");
    expect(res.status).toBe(400);
  });

  it("returns 503 when docker is not available", async () => {
    mockEnvManager.getEnv.mockReturnValue(makeEnv({ dockerfile: "FROM node" }));
    mockContainerManager.containerManager.checkDocker.mockReturnValue(false);
    const res = await req("POST", "/envs/test-env/build");
    expect(res.status).toBe(503);
  });

  it("builds successfully", async () => {
    mockEnvManager.getEnv.mockReturnValue(makeEnv({ dockerfile: "FROM node" }));
    mockContainerManager.containerManager.buildImageStreaming.mockResolvedValue({
      success: true,
      log: "Build completed",
    });
    const res = await req("POST", "/envs/test-env/build");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.success).toBe(true);
    expect(body.imageTag).toBe("zencode-env-test-env:latest");
  });

  it("returns 500 on build failure", async () => {
    mockEnvManager.getEnv.mockReturnValue(makeEnv({ dockerfile: "FROM node" }));
    mockContainerManager.containerManager.buildImageStreaming.mockResolvedValue({
      success: false,
      log: "Build failed",
    });
    const res = await req("POST", "/envs/test-env/build");
    expect(res.status).toBe(500);
    const body = await res.json();
    expect(body.success).toBe(false);
  });
});

describe("GET /envs/:slug/build-status", () => {
  it("returns 404 for non-existent env", async () => {
    const res = await req("GET", "/envs/missing/build-status");
    expect(res.status).toBe(404);
  });

  it("returns build status", async () => {
    mockEnvManager.getEnv.mockReturnValue(makeEnv({
      buildStatus: "success",
      imageTag: "zencode-env-test:latest",
    }));
    const res = await req("GET", "/envs/test-env/build-status");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.buildStatus).toBe("success");
    expect(body.imageTag).toBe("zencode-env-test:latest");
  });
});

describe("GET /docker/base-image", () => {
  it("returns image existence status", async () => {
    mockContainerManager.containerManager.imageExists.mockReturnValue(true);
    const res = await req("GET", "/docker/base-image");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.exists).toBe(true);
    expect(body.image).toBe("zencode:latest");
  });
});

describe("GET /images/:tag/status", () => {
  it("returns image pull state", async () => {
    mockImagePullManager.imagePullManager.getState.mockReturnValue({
      status: "pulling",
      log: ["Pulling..."],
      error: null,
    });
    const res = await req("GET", "/images/ubuntu%3Alatest/status");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.status).toBe("pulling");
  });
});

describe("POST /images/:tag/pull", () => {
  it("returns 503 when docker is not available", async () => {
    mockContainerManager.containerManager.checkDocker.mockReturnValue(false);
    const res = await req("POST", "/images/ubuntu%3Alatest/pull");
    expect(res.status).toBe(503);
  });

  it("starts pull and returns state", async () => {
    const res = await req("POST", "/images/ubuntu%3Alatest/pull");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockImagePullManager.imagePullManager.pull).toHaveBeenCalled();
  });
});
