import type { Denops } from "jsr:@denops/core@^7.0.0";
import { as, assert, ensure, is } from "jsr:@core/unknownutil@^4.0.0";
import { Client, Session } from "jsr:@lambdalisue/messagepack-rpc@^2.1.1";
import { errorDeserializer, errorSerializer } from "./error.ts";

export async function main(denops: Denops): Promise<void> {
  const addr = Deno.env.get("DENOPS_TEST_ADDRESS");
  if (!addr) {
    throw new Error("Environment variable 'DENOPS_TEST_ADDRESS' is not set");
  }
  const conn = await Deno.connect(JSON.parse(addr));
  const session = new Session(conn.readable, conn.writable, {
    errorSerializer,
  });
  session.onInvalidMessage = (message) => {
    console.error(`[denops-test] Unexpected message: ${message}`);
  };
  session.onMessageError = (err, message) => {
    console.error(
      `[denops-test] Unexpected error occured for message ${message}: ${err}`,
    );
  };
  session.start();
  const client = new Client(session, {
    errorDeserializer,
  });
  session.dispatcher = {
    invoke: (name, args) => {
      assert(name, is.String);
      assert(args, is.Array);
      return invoke(denops, name, args);
    },
  };
  denops.dispatcher = new Proxy({}, {
    get: (_, prop) => {
      assert(prop, is.String);
      return (...args: unknown[]) => {
        return client.call("dispatch", prop, args);
      };
    },
    set: () => {
      throw new Error("This dispatcher is for test and read-only");
    },
    deleteProperty: () => {
      throw new Error("This dispatcher is for test and read-only");
    },
  });
}

function invoke(
  denops: Denops,
  name: string,
  args: unknown[],
): Promise<unknown> {
  switch (name) {
    case "redraw":
      return denops.redraw(...ensure(args, isRedrawArgs));
    case "call":
      return denops.call(...ensure(args, isCallArgs));
    case "batch":
      return denops.batch(...ensure(args, isBatchArgs));
    case "cmd":
      return denops.cmd(...ensure(args, isCmdArgs));
    case "eval":
      return denops.eval(...ensure(args, isEvalArgs));
    case "dispatch":
      return denops.dispatch(...ensure(args, isDispatchArgs));
    default:
      throw new Error(`Unknown denops method '${name}' is specified`);
  }
}

const isRedrawArgs = is.TupleOf([as.Optional(is.Boolean)] as const);

const isCallArgs = (v: unknown): v is [string, ...unknown[]] => {
  return is.Array(v) && is.String(v[0]);
};

const isBatchArgs = is.ArrayOf(isCallArgs);

const isCmdArgs = is.TupleOf([is.String, as.Optional(is.Record)] as const);

const isEvalArgs = is.TupleOf([is.String, as.Optional(is.Record)] as const);

const isDispatchArgs = (v: unknown): v is [string, string, ...unknown[]] => {
  return is.Array(v) && is.String(v[0]) && is.String(v[1]);
};