import fs from "fs";
import os from "os";
import path from "path";
import { describe, expect, it } from "vitest";
import {
  buildExecutionEnv,
  buildPlaywrightEnv,
  buildPortMap,
  buildTaskExecutionEnv,
  finalizeString,
  normalizeSocketHost,
  numericPortFromUrl,
  resolveRuntimeInstanceConfigs,
  resolveServiceStateDir,
  resolveTemplateString,
  rewriteUrlPort,
  socketFromUrl,
} from "./template.mjs";

function makeRuntimeConfig(name, local, extras = {}) {
  return {
    name,
    stateDir: extras.stateDir,
    runtimeId: extras.runtimeId,
    testkit: {
      local,
      serviceEnv: extras.serviceEnv || {},
      databaseFrom: extras.databaseFrom,
      database: extras.database,
      templateContext: extras.templateContext,
    },
  };
}

describe("runner-template", () => {
  it("builds port maps and detects collisions", () => {
    const configs = [
      makeRuntimeConfig("api", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
      makeRuntimeConfig("frontend", { port: 3001, baseUrl: "http://127.0.0.1:{port}" }),
    ];

    expect([...buildPortMap(configs, "runtime-2").entries()]).toEqual([
      ["api", 3100],
      ["frontend", 3101],
    ]);
    expect([...buildPortMap(configs, "runtime-1", { index: 1, stride: 2 }).entries()]).toEqual([
      ["api", 3200],
      ["frontend", 3201],
    ]);

    expect(() =>
      buildPortMap(
        [
          makeRuntimeConfig("api", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
          makeRuntimeConfig("other", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
        ],
        "runtime-1",
        { index: 0, stride: 2 }
      )
    ).toThrow("Runtime port collision");
  });

  it("resolves template strings and URL rewrites", () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-template-"));
    const apiStateDir = path.join(tmpDir, "api");
    fs.mkdirSync(apiStateDir, { recursive: true });
    fs.writeFileSync(
      path.join(apiStateDir, "database_url"),
      "postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
    );

    const context = {
      runtimeId: "runtime-2",
      serviceName: "frontend",
      serviceStateDir: "/tmp/state",
      portMap: new Map([
        ["frontend", 3200],
        ["api", 3100],
      ]),
      baseUrlByService: new Map([["api", "http://127.0.0.1:3100"]]),
      readyUrlByService: new Map([["api", "http://127.0.0.1:3100/health"]]),
      stateDirByService: new Map([["api", apiStateDir]]),
      urlMappings: [["http://api:3000", "http://127.0.0.1:3100"]],
      leaseId: "lease-1",
      leaseDir: "/tmp/lease-1",
    };

    expect(resolveTemplateString("{runtime}:{service}:{lease}", context)).toBe(
      "runtime-2:frontend:lease-1"
    );
    expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
    expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
      "127.0.0.1:55432/runtime_db"
    );
    expect(finalizeString("API={baseUrl:api} OLD=http://api:3000", context)).toBe(
      "API=http://127.0.0.1:3100 OLD=http://127.0.0.1:3100"
    );
    expect(rewriteUrlPort("http://127.0.0.1:3000/health", 3200)).toBe(
      "http://127.0.0.1:3200/health"
    );
  });

  it("builds runtime configs and execution env", () => {
    const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-"));
    const api = makeRuntimeConfig(
      "api",
      {
        cwd: ".",
        start: "npm run api",
        port: 3000,
        baseUrl: "http://127.0.0.1:{port}",
        readyUrl: "http://127.0.0.1:{port}/health",
        env: {
          PORT: "{port}",
        },
      },
      {
        serviceEnv: {
          API_KEY: "secret",
        },
      }
    );
    const frontend = makeRuntimeConfig("frontend", {
      cwd: "frontend",
      start: "npm run web",
      port: 3001,
      baseUrl: "http://127.0.0.1:{port}",
      readyUrl: "http://127.0.0.1:{port}",
      env: {
        NEXT_PUBLIC_API_URL: "{baseUrl:api}",
        API_DB_HOST: "{dbHost:api}",
      },
    });

    const resolved = resolveRuntimeInstanceConfigs([api, frontend], "runtime-2", runtimeDir, {
      graphDirName: "api__frontend",
      portNamespaceIndex: 0,
      portNamespaceStride: 2,
    });
    expect(resolved[0].testkit.local.port).toBe(3100);
    expect(resolved[0].runtimeLabel).toBe("api__frontend/runtime-2");
    expect(resolveServiceStateDir(runtimeDir, api)).toBe(`${runtimeDir}/services/api`);

    fs.mkdirSync(path.join(runtimeDir, "services", "api"), { recursive: true });
    fs.writeFileSync(
      path.join(runtimeDir, "services", "api", "database_url"),
      "postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
    );

    expect(
      buildExecutionEnv(
        resolved[1],
        {
          ...resolved[1].testkit.local.env,
          DATABASE_URL: "gone",
        },
        {
          PATH: "/usr/bin",
        }
      )
    ).toEqual({
      PATH: "/usr/bin",
      NEXT_PUBLIC_API_URL: "http://127.0.0.1:3100",
      API_DB_HOST: "127.0.0.1",
      TESTKIT_ACTIVE: "1",
      TESTKIT_RUNTIME_ID: "runtime-2",
    });

    expect(
      buildTaskExecutionEnv(
        resolved[1],
        {
          leaseId: "lease-1",
          leaseDir: "/tmp/lease-1",
        },
        {},
        {}
      )
    ).toMatchObject({
      TESTKIT_RUNTIME_ID: "runtime-2",
      TESTKIT_LEASE_ID: "lease-1",
      TESTKIT_LEASE_DIR: "/tmp/lease-1",
    });

    expect(
      buildPlaywrightEnv(
        { runtimeId: "runtime-1", testkit: { serviceEnv: {}, templateContext: {} } },
        "http://localhost:3000",
        { leaseId: "lease-2", leaseDir: "/tmp/lease-2" },
        {}
      )
    ).toMatchObject({
      BASE_URL: "http://localhost:3000",
      TESTKIT_RUNTIME_ID: "runtime-1",
      TESTKIT_LEASE_ID: "lease-2",
      PLAYWRIGHT_HTML_OPEN: "never",
    });
  });

  it("parses runtime sockets", () => {
    expect(numericPortFromUrl("http://localhost:3000")).toBe(3000);
    expect(socketFromUrl("http://localhost:3000")).toEqual({
      host: "127.0.0.1",
      port: 3000,
    });
    expect(normalizeSocketHost("[::1]")).toBe("::1");
  });
});
