import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { LocalProjectMemoryStore } from "./project-memory.js";
import { ProjectMemorySyncClient, normalizeTeamMemoryProjectKey } from "./project-memory-sync.js";
import { AgentRuntime } from "../runtime/agent-runtime.js";

const roots: string[] = [];
const originalFetch = globalThis.fetch;

afterEach(() => {
  globalThis.fetch = originalFetch;
  while (roots.length > 0) {
    rmSync(roots.pop() as string, { force: true, recursive: true });
  }
});

describe("project memory sync client", () => {
  it("normalizes HTTPS and SSH git remotes into one project key format", () => {
    expect(normalizeTeamMemoryProjectKey("https://github.com/OpenAI/Best-Agent.git")).toBe(
      "github.com/openai/best-agent",
    );
    expect(normalizeTeamMemoryProjectKey("git@github.com:OpenAI/Best-Agent.git")).toBe(
      "github.com/openai/best-agent",
    );
  });

  it("falls back to local-only when no git remote is configured", async () => {
    const workspaceRoot = mkdtempSync(`${tmpdir()}/best-agent-project-memory-sync-local-only-`);
    roots.push(workspaceRoot);
    const storageRoot = join(workspaceRoot, "state");
    execFileSync("git", ["init"], { cwd: workspaceRoot, stdio: "ignore" });
    const store = new LocalProjectMemoryStore({ cwd: workspaceRoot, storageRoot });
    const requests: string[] = [];
    const client = new ProjectMemorySyncClient({
      cwd: workspaceRoot,
      fetchFn: async (input) => {
        requests.push(String(input));
        return new Response(JSON.stringify({ entries: [] }), {
          headers: { "content-type": "application/json" },
          status: 200,
        });
      },
      projectMemory: store,
      url: "http://team-memory.test",
    });

    await client.syncNow();

    expect(requests).toEqual([]);
    expect(client.getStatus()).toMatchObject({
      enabled: false,
      mode: "local-only",
      reason: "missing-git-remote",
    });
  });

  it("pulls server memory, detects conflicts, and patches local changes back to the server", async () => {
    const workspaceRoot = mkdtempSync(`${tmpdir()}/best-agent-project-memory-sync-client-`);
    roots.push(workspaceRoot);
    const storageRoot = join(workspaceRoot, "state");
    execFileSync("git", ["init"], { cwd: workspaceRoot, stdio: "ignore" });
    execFileSync(
      "git",
      ["remote", "add", "origin", "https://github.com/acme/team-memory-fixture.git"],
      {
        cwd: workspaceRoot,
        stdio: "ignore",
      },
    );
    const store = new LocalProjectMemoryStore({ cwd: workspaceRoot, storageRoot });
    store.set("Repository uses pnpm.");

    const warnings: string[] = [];
    const requests: Array<{ body?: string; method: string; url: string }> = [];
    let serverEntries = [
      {
        checksum: "server-manual",
        content: "Repository uses npm.",
        key: "manual",
        updatedAt: new Date(Date.now() - 60_000).toISOString(),
      },
      {
        checksum: "server-generated",
        content: "Always run tests before shipping.",
        key: "generated:tests",
        updatedAt: new Date().toISOString(),
      },
    ];
    const client = new ProjectMemorySyncClient({
      cwd: workspaceRoot,
      fetchFn: async (input, init) => {
        const method = init?.method ?? "GET";
        requests.push({
          body: typeof init?.body === "string" ? init.body : undefined,
          method,
          url: String(input),
        });
        if (method === "PATCH") {
          const body = JSON.parse(String(init?.body)) as {
            entries: Array<{ content: string; key: string }>;
          };
          serverEntries = body.entries.map((entry) => ({
            checksum: entry.content,
            content: entry.content,
            key: entry.key,
            updatedAt: new Date().toISOString(),
          }));
          return new Response(
            JSON.stringify({ entries: serverEntries, synced: body.entries.length }),
            {
              headers: { "content-type": "application/json" },
              status: 200,
            },
          );
        }
        return new Response(JSON.stringify({ entries: serverEntries }), {
          headers: { "content-type": "application/json" },
          status: 200,
        });
      },
      log: {
        warn(message) {
          warnings.push(message);
        },
      },
      projectMemory: store,
      url: "http://team-memory.test",
    });

    expect(requests).toEqual([]);
    expect(client.getStatus()).toMatchObject({
      enabled: false,
      mode: "local-only",
      reason: "missing-git-remote",
    });

    await client.syncNow();

    expect(requests.map((request) => request.method)).toEqual(["GET", "PATCH"]);
    expect(requests[0]?.url).toContain(
      "/api/v1/team-memory?project=github.com%2Facme%2Fteam-memory-fixture",
    );
    expect(JSON.parse(requests[1]?.body ?? "{}")).toMatchObject({
      entries: [{ content: "Repository uses pnpm.", key: "manual" }],
    });
    expect(warnings.some((message) => message.includes("Team memory conflict"))).toBe(true);
    expect(store.get().content).toContain("Repository uses pnpm.");
    expect(client.getStatus()).toMatchObject({
      enabled: true,
      lastConflictAt: expect.any(String),
      lastPullAt: expect.any(String),
      lastPushAt: expect.any(String),
      mode: "shared",
      projectKey: "github.com/acme/team-memory-fixture",
    });
  });

  it("integrates startup pull and debounced push through AgentRuntime", async () => {
    const workspaceRoot = mkdtempSync(`${tmpdir()}/best-agent-project-memory-runtime-sync-`);
    roots.push(workspaceRoot);
    const storageRoot = join(workspaceRoot, "state");
    execFileSync("git", ["init"], { cwd: workspaceRoot, stdio: "ignore" });
    execFileSync(
      "git",
      ["remote", "add", "origin", "https://github.com/acme/runtime-team-memory.git"],
      {
        cwd: workspaceRoot,
        stdio: "ignore",
      },
    );

    const requests: string[] = [];
    let serverEntries = [
      {
        checksum: "remote-manual",
        content: "Always run tests before shipping.",
        key: "manual",
        updatedAt: new Date().toISOString(),
      },
    ];
    globalThis.fetch = Object.assign(
      async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
        const method = init?.method ?? "GET";
        requests.push(`${method} ${String(input)}`);
        if (method === "PATCH") {
          const body = JSON.parse(String(init?.body)) as {
            entries: Array<{ content: string; key: string }>;
          };
          serverEntries = body.entries.map((entry) => ({
            checksum: entry.content,
            content: entry.content,
            key: entry.key,
            updatedAt: new Date().toISOString(),
          }));
          return new Response(
            JSON.stringify({ entries: serverEntries, synced: body.entries.length }),
            {
              headers: { "content-type": "application/json" },
              status: 200,
            },
          );
        }
        return new Response(JSON.stringify({ entries: serverEntries }), {
          headers: { "content-type": "application/json" },
          status: 200,
        });
      },
      { preconnect() {} },
    );

    const runtime = new AgentRuntime({
      cwd: workspaceRoot,
      storageRoot,
      teamMemory: {
        debounceMs: 5,
        url: "http://team-memory.test",
      },
    });

    await waitForAsync(() =>
      runtime.getProjectMemory().memory.content.includes("Always run tests before shipping."),
    );
    expect(runtime.getProjectMemory().memory.sync).toMatchObject({
      enabled: true,
      lastPullAt: expect.any(String),
      mode: "shared",
      projectKey: "github.com/acme/runtime-team-memory",
    });

    runtime.setProjectMemory("Repository uses pnpm.");

    await waitForAsync(() =>
      serverEntries.some((entry) => entry.content === "Repository uses pnpm."),
    );
    expect(runtime.getProjectMemory().memory.sync).toMatchObject({
      enabled: true,
      lastPushAt: expect.any(String),
      mode: "shared",
      projectKey: "github.com/acme/runtime-team-memory",
    });
    expect(serverEntries).toEqual([
      expect.objectContaining({
        content: "Repository uses pnpm.",
        key: "manual",
      }),
    ]);

    await runtime.close();
  });
});

async function waitForAsync(predicate: () => boolean): Promise<void> {
  for (let attempt = 0; attempt < 50; attempt += 1) {
    if (predicate()) {
      return;
    }
    await new Promise((resolve) => {
      setTimeout(resolve, 10);
    });
  }
  throw new Error("Timed out waiting for test condition.");
}
