import { createClient } from "@libsql/client";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { Effect, Layer } from "effect";
import { afterAll, beforeAll, expect, test } from "@effect/vitest";

import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk";
import { makeScopedExecutor } from "@executor-js/api/server";

import { createSelfHostDb, SelfHostDb } from "./db/self-host-db";
import { SelfHostScopedExecutorSeams } from "./execution";
import type { SelfHostPlugins } from "./plugins";

// In v2 a connection IS the credential: its inline `value` is written through the
// default writable provider — here the encrypted-secrets provider, which stores
// an AES-GCM payload at rest. This test registers an integration, attaches an
// org connection carrying a plaintext needle, and asserts the needle never
// reaches the SQLite file while the versioned "v1." ciphertext does.
const dataDir = mkdtempSync(join(tmpdir(), "eh-secrets-"));
const dbPath = join(dataDir, "data.db");
process.env.EXECUTOR_DATA_DIR = dataDir;
process.env.EXECUTOR_SECRET_KEY = "integration-test-master-key";

const createScopedExecutor = (
  accountId: string,
  organizationId: string,
  organizationName: string,
) =>
  makeScopedExecutor<SelfHostPlugins>(accountId, organizationId, organizationName).pipe(
    Effect.provide(SelfHostScopedExecutorSeams),
  );

let dbLayer!: Layer.Layer<SelfHostDb>;
let dbHandle: Awaited<ReturnType<typeof createSelfHostDb>> | undefined;

beforeAll(async () => {
  dbHandle = await createSelfHostDb({
    path: dbPath,
    namespace: "executor_selfhost",
    version: "1.0.0",
  });
  dbLayer = Layer.succeed(SelfHostDb)(dbHandle);
});
afterAll(async () => {
  await dbHandle?.close();
});

const TINY_SPEC = JSON.stringify({
  openapi: "3.0.0",
  info: { title: "Tiny", version: "1.0.0" },
  servers: [{ url: "https://httpbin.org" }],
  paths: {
    "/get": {
      get: {
        operationId: "httpGet",
        summary: "GET",
        responses: { "200": { description: "ok" } },
      },
    },
  },
});

const NEEDLE = "PLAINTEXT_NEEDLE_9f3a";

test("a connection value is stored encrypted at rest by the 'encrypted' provider", async () => {
  const created = await Effect.runPromise(
    Effect.gen(function* () {
      const admin = yield* createScopedExecutor("admin", "default-org", "Default");
      yield* admin.openapi.addSpec({
        spec: { kind: "blob", value: TINY_SPEC },
        slug: "tiny",
        baseUrl: "",
      });
      // The connection's inline `value` is opaque to core (D11) — it is written
      // through the default writable provider regardless of the template slug.
      return yield* admin.connections.create({
        owner: "org",
        name: ConnectionName.make("gh"),
        integration: IntegrationSlug.make("tiny"),
        template: AuthTemplateSlug.make("bearer"),
        value: NEEDLE,
      });
    }).pipe(Effect.provide(dbLayer), Effect.scoped),
  );

  // The first writable provider is the encrypted one — it handled the write.
  expect(String(created.provider)).toBe("encrypted");

  // Inspect the real SQLite file through a SEPARATE libSQL connection: the
  // plaintext must NOT appear anywhere, and a versioned AES-GCM payload ("v1.")
  // must be present. Reading through an independent connection also exercises the
  // cross-connection visibility of FumaDB's writes.
  const db = createClient({ url: `file:${dbPath}` });
  const tables = (await db.execute("SELECT name FROM sqlite_master WHERE type='table'")).rows.map(
    // oxlint-disable-next-line executor/no-redundant-primitive-cast -- boundary: sqlite_master.name is TEXT; narrow libSQL's SQLValue to string for the table list
    (r) => r.name as string,
  );
  const cells: string[] = [];
  for (const name of tables) {
    const rows = (await db.execute(`SELECT * FROM "${name}"`)).rows;
    for (const row of rows) {
      for (const value of Object.values(row)) {
        // Plugin-storage data is a BLOB (libSQL returns ArrayBuffer); decode it.
        if (typeof value === "string") cells.push(value);
        else if (value instanceof ArrayBuffer) cells.push(Buffer.from(value).toString("utf8"));
        else if (ArrayBuffer.isView(value))
          cells.push(
            Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("utf8"),
          );
      }
    }
  }
  db.close();

  expect(cells.some((c) => c.includes(NEEDLE))).toBe(false);
  expect(cells.some((c) => c.includes("v1."))).toBe(true);
});
