import { describe, test, expect, vi } from "vitest";
import { ConnectError, Code } from "@connectrpc/connect";
import type { Client } from "@connectrpc/connect";
import type { grackle } from "@grackle-ai/common";
import type { AuthContext } from "@grackle-ai/auth";
import { taskTools } from "./task.js";

type GrackleClient = Client<typeof grackle.GrackleOrchestration>;

/** Helper to find a tool definition by name. */
const getTool = (name: string) => taskTools.find((t) => t.name === name)!;

/** Create a mock Grackle client with all task-related methods stubbed. */
function createMockClient(): GrackleClient {
  return {
    listTasks: vi.fn(),
    createTask: vi.fn(),
    getTask: vi.fn(),
    updateTask: vi.fn(),
    startTask: vi.fn(),
    deleteTask: vi.fn(),
    completeTask: vi.fn(),
    resumeTask: vi.fn(),
  } as unknown as GrackleClient;
}

describe("task_list", () => {
  /** Should return task summaries with human-readable status. */
  test("happy path returns task list", async () => {
    const mockClient = createMockClient();
    (mockClient.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue({
      tasks: [
        {
          id: "t1",
          title: "Fix bug",
          status: 1,
          branch: "main",
          latestSessionId: "s1",
          sortOrder: 0,
          parentTaskId: "",
          depth: 0,
          childTaskIds: [],
        },
      ],
    });

    const result = await getTool("task_list").handler(
      { workspaceId: "proj-1" }, { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed).toHaveLength(1);
    expect(parsed[0].id).toBe("t1");
    expect(parsed[0].title).toBe("Fix bug");
    expect(typeof parsed[0].status).toBe("string");
    expect(mockClient.listTasks).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      search: "",
      status: "",
    });
  });

  /** Should pass search param through to client.listTasks. */
  test("passes search param to client", async () => {
    const mockClient = createMockClient();
    (mockClient.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue({
      tasks: [],
    });

    await getTool("task_list").handler(
      { workspaceId: "proj-1", search: "login bug" },
      { orchestration: mockClient },
    );

    expect(mockClient.listTasks).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      search: "login bug",
      status: "",
    });
  });

  /** Should pass status param through to client.listTasks. */
  test("passes status param to client", async () => {
    const mockClient = createMockClient();
    (mockClient.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue({
      tasks: [],
    });

    await getTool("task_list").handler(
      { workspaceId: "proj-1", status: "working" },
      { orchestration: mockClient },
    );

    expect(mockClient.listTasks).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      search: "",
      status: "working",
    });
  });

  /** Should send empty strings for optional params when not provided (backwards compat). */
  test("sends empty strings when optional params omitted", async () => {
    const mockClient = createMockClient();
    (mockClient.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue({
      tasks: [],
    });

    await getTool("task_list").handler(
      { workspaceId: "proj-1" }, { orchestration: mockClient },
    );

    expect(mockClient.listTasks).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      search: "",
      status: "",
    });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.listTasks as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("not found", Code.NotFound),
    );

    const result = await getTool("task_list").handler(
      { workspaceId: "no-such" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });
});

describe("task_create", () => {
  /** Should default parentTaskId to empty string and dependsOn to empty array. */
  test("happy path with defaults", async () => {
    const mockClient = createMockClient();
    (mockClient.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t2",
      title: "New task",
      status: 0,
    });

    const result = await getTool("task_create").handler(
      { workspaceId: "proj-1", title: "New task" },
      { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.id).toBe("t2");
    expect(mockClient.createTask).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      title: "New task",
      description: "",
      dependsOn: [],
      parentTaskId: "",
      canDecompose: false,
      defaultPersonaId: "",
      tokenBudget: 0,
      costBudgetMillicents: 0,
    });
  });

  /** Should pass provided description and dependsOn. */
  test("passes description and dependsOn when provided", async () => {
    const mockClient = createMockClient();
    (mockClient.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t3",
      title: "Dep task",
      status: 0,
    });

    await getTool("task_create").handler(
      {
        workspaceId: "proj-1",
        title: "Dep task",
        description: "Details here",
        dependsOn: ["t1", "t2"],
      },
      { orchestration: mockClient },
    );

    expect(mockClient.createTask).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      title: "Dep task",
      description: "Details here",
      dependsOn: ["t1", "t2"],
      parentTaskId: "",
      canDecompose: false,
      defaultPersonaId: "",
      tokenBudget: 0,
      costBudgetMillicents: 0,
    });
  });

  /** Should pass tokenBudget and costBudgetMillicents to client. */
  test("passes token_budget and cost_budget_millicents to client", async () => {
    const mockClient = createMockClient();
    (mockClient.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t4",
      title: "Budget task",
      status: 0,
    });

    await getTool("task_create").handler(
      {
        workspaceId: "proj-1",
        title: "Budget task",
        tokenBudget: 50000,
        costBudgetMillicents: 100,
      },
      { orchestration: mockClient },
    );

    expect(mockClient.createTask).toHaveBeenCalledWith({
      workspaceId: "proj-1",
      title: "Budget task",
      description: "",
      dependsOn: [],
      parentTaskId: "",
      canDecompose: false,
      defaultPersonaId: "",
      tokenBudget: 50000,
      costBudgetMillicents: 100,
    });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.createTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("invalid", Code.InvalidArgument),
    );

    const result = await getTool("task_create").handler(
      { workspaceId: "proj-1", title: "Bad" },
      { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.code).toBe("INVALID_ARGUMENT");
  });
});

describe("task_show", () => {
  /** Should call getTask and return the task with human-readable status. */
  test("happy path returns task details", async () => {
    const mockClient = createMockClient();
    (mockClient.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t1",
      title: "Fix bug",
      status: 2,
      description: "Fix the important bug",
    });

    const result = await getTool("task_show").handler(
      { taskId: "t1" }, { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.id).toBe("t1");
    // taskToJson converts status via taskStatusToString; if unrecognized it falls back to raw value
    expect(parsed).toHaveProperty("status");
    expect(mockClient.getTask).toHaveBeenCalledWith({ id: "t1" });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("not found", Code.NotFound),
    );

    const result = await getTool("task_show").handler(
      { taskId: "no-such" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });
});

describe("task_update", () => {
  /** Should convert status string to enum value via taskStatusToEnum. */
  test("converts status string to enum when provided", async () => {
    const mockClient = createMockClient();
    (mockClient.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t1",
      title: "Updated",
      status: 2,
    });

    const result = await getTool("task_update").handler(
      { taskId: "t1", title: "Updated", status: "working" },
      { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.id).toBe("t1");
    // Verify the call was made — status should be a numeric enum value, not the string
    const callArgs = (mockClient.updateTask as ReturnType<typeof vi.fn>).mock.calls[0][0];
    expect(typeof callArgs.status).toBe("number");
    expect(callArgs.status).not.toBe(0);
  });

  /** Should send status 0 when no status is provided. */
  test("sends status 0 when no status provided", async () => {
    const mockClient = createMockClient();
    (mockClient.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t1",
      title: "Just title",
      status: 1,
    });

    await getTool("task_update").handler(
      { taskId: "t1", title: "Just title" },
      { orchestration: mockClient },
    );

    const callArgs = (mockClient.updateTask as ReturnType<typeof vi.fn>).mock.calls[0][0];
    expect(callArgs.status).toBe(0);
  });

  /** Should pass budget fields when provided. */
  test("passes budget fields when provided", async () => {
    const mockClient = createMockClient();
    (mockClient.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t1",
      title: "Budget update",
      status: 1,
    });

    await getTool("task_update").handler(
      { taskId: "t1", tokenBudget: 25000, costBudgetMillicents: 500 },
      { orchestration: mockClient },
    );

    const callArgs = (mockClient.updateTask as ReturnType<typeof vi.fn>).mock.calls[0][0];
    expect(callArgs.tokenBudget).toBe(25000);
    expect(callArgs.costBudgetMillicents).toBe(500);
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.updateTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("not found", Code.NotFound),
    );

    const result = await getTool("task_update").handler(
      { taskId: "no-such" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });
});

describe("task_start", () => {
  /** Should call startTask with all args and defaults. */
  test("happy path returns response", async () => {
    const mockClient = createMockClient();
    (mockClient.startTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "s1",
      pipeFd: 0,
    });

    const result = await getTool("task_start").handler(
      { taskId: "t1" }, { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.sessionId).toBe("s1");
    expect(parsed.taskId).toBe("t1");
    expect(mockClient.startTask).toHaveBeenCalledWith({
      taskId: "t1",
      personaId: "",
      environmentId: "",
      notes: "",
      pipe: "",
      parentSessionId: "",
    });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.startTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("precondition", Code.FailedPrecondition),
    );

    const result = await getTool("task_start").handler(
      { taskId: "t1" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });

  /** Should reject when scoped auth and target is not a descendant. */
  test("rejects when scoped auth and target is not a descendant", async () => {
    const scopedAuth: AuthContext = {
      type: "scoped",
      taskId: "parent-task",
      workspaceId: "proj-1",
      personaId: "p-1",
      taskSessionId: "sess-1",
    };
    const mockClient = createMockClient();
    (mockClient.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "unrelated-task",
      parentTaskId: "",
    });

    const result = await getTool("task_start").handler(
      { taskId: "unrelated-task" }, { orchestration: mockClient },
      scopedAuth,
    );

    expect(result.isError).toBe(true);
    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.code).toBe("PERMISSION_DENIED");
  });

  /** Should pass when scoped auth and target is a descendant. */
  test("passes when scoped auth and target is a descendant", async () => {
    const scopedAuth: AuthContext = {
      type: "scoped",
      taskId: "parent-task",
      workspaceId: "proj-1",
      personaId: "p-1",
      taskSessionId: "sess-1",
    };
    const mockClient = createMockClient();
    (mockClient.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "child-task",
      parentTaskId: "parent-task",
    });
    (mockClient.startTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "s1",
      pipeFd: 0,
    });

    const result = await getTool("task_start").handler(
      { taskId: "child-task" }, { orchestration: mockClient },
      scopedAuth,
    );

    expect(result.isError).toBeUndefined();
    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.sessionId).toBe("s1");
  });
});

describe("task_delete", () => {
  /** Should call deleteTask and return success. */
  test("happy path returns success", async () => {
    const mockClient = createMockClient();
    (mockClient.deleteTask as ReturnType<typeof vi.fn>).mockResolvedValue({});

    const result = await getTool("task_delete").handler(
      { taskId: "t1" }, { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.success).toBe(true);
    expect(mockClient.deleteTask).toHaveBeenCalledWith({ id: "t1" });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.deleteTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("not found", Code.NotFound),
    );

    const result = await getTool("task_delete").handler(
      { taskId: "no-such" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });
});

describe("task_complete", () => {
  /** Should call completeTask and return the task with human-readable status. */
  test("happy path returns completed task", async () => {
    const mockClient = createMockClient();
    (mockClient.completeTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "t1",
      title: "Done task",
      status: 3,
    });

    const result = await getTool("task_complete").handler(
      { taskId: "t1" }, { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.id).toBe("t1");
    expect(parsed).toHaveProperty("status");
    expect(mockClient.completeTask).toHaveBeenCalledWith({ id: "t1" });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.completeTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("not found", Code.NotFound),
    );

    const result = await getTool("task_complete").handler(
      { taskId: "no-such" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });

  /** Should reject when scoped auth and target is not a descendant. */
  test("rejects when scoped auth and target is not a descendant", async () => {
    const scopedAuth: AuthContext = {
      type: "scoped",
      taskId: "parent-task",
      workspaceId: "proj-1",
      personaId: "p-1",
      taskSessionId: "sess-1",
    };
    const mockClient = createMockClient();
    (mockClient.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "unrelated-task",
      parentTaskId: "",
    });

    const result = await getTool("task_complete").handler(
      { taskId: "unrelated-task" }, { orchestration: mockClient },
      scopedAuth,
    );

    expect(result.isError).toBe(true);
    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.code).toBe("PERMISSION_DENIED");
  });

  /** Should pass when scoped auth and target is a descendant. */
  test("passes when scoped auth and target is a descendant", async () => {
    const scopedAuth: AuthContext = {
      type: "scoped",
      taskId: "parent-task",
      workspaceId: "proj-1",
      personaId: "p-1",
      taskSessionId: "sess-1",
    };
    const mockClient = createMockClient();
    (mockClient.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "child-task",
      parentTaskId: "parent-task",
    });
    (mockClient.completeTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "child-task",
      title: "Done",
      status: 3,
    });

    const result = await getTool("task_complete").handler(
      { taskId: "child-task" }, { orchestration: mockClient },
      scopedAuth,
    );

    expect(result.isError).toBeUndefined();
    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.id).toBe("child-task");
  });
});

describe("task_resume", () => {
  /** Should call resumeTask and return the session. */
  test("happy path returns session", async () => {
    const mockClient = createMockClient();
    (mockClient.resumeTask as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "s1",
      status: "running",
    });

    const result = await getTool("task_resume").handler(
      { taskId: "t1" }, { orchestration: mockClient },
    );
    const parsed = JSON.parse(result.content[0].text);

    expect(parsed.id).toBe("s1");
    expect(mockClient.resumeTask).toHaveBeenCalledWith({ id: "t1" });
  });

  /** Should return a structured error on ConnectError. */
  test("ConnectError returns isError result", async () => {
    const mockClient = createMockClient();
    (mockClient.resumeTask as ReturnType<typeof vi.fn>).mockRejectedValue(
      new ConnectError("not found", Code.NotFound),
    );

    const result = await getTool("task_resume").handler(
      { taskId: "no-such" }, { orchestration: mockClient },
    );

    expect(result.isError).toBe(true);
  });
});

