import { describe, it, expect } from "@effect/vitest";
import { Effect, Schema } from "effect";
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
import { FileSystem } from "effect";
import { join } from "node:path";

import { ExecutorFileConfig } from "./schema";
import { ConfigParseError, loadConfig } from "./load";
import {
  addSourceToConfig,
  removeSourceFromConfig,
  writeConfig,
  addSecretToConfig,
  removeSecretFromConfig,
} from "./write";

const decodeExecutorFileConfig = Schema.decodeUnknownSync(ExecutorFileConfig);

const withTmpDir = <A, E>(fn: (dir: string) => Effect.Effect<A, E, FileSystem.FileSystem>) =>
  Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem;
    const dir = yield* fs.makeTempDirectoryScoped({
      prefix: "executor-config-test-",
    });
    return yield* fn(dir);
  }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer));

describe("ExecutorFileConfig schema", () => {
  it("decodes a minimal config", () => {
    const raw = { sources: [] };
    const result = decodeExecutorFileConfig(raw);
    expect(result.sources).toEqual([]);
  });

  it("decodes a full config with all source types", () => {
    const raw = {
      name: "test",
      sources: [
        {
          kind: "openapi",
          spec: "https://example.com/openapi.json",
          namespace: "example",
          headers: {
            Authorization: { value: "secret-public-ref:my-token", prefix: "Bearer " },
          },
        },
        {
          kind: "graphql",
          endpoint: "https://example.com/graphql",
          namespace: "gql",
        },
        {
          kind: "mcp",
          transport: "remote",
          name: "Remote MCP",
          endpoint: "https://mcp.example.com/sse",
        },
        {
          kind: "mcp",
          transport: "stdio",
          name: "Local MCP",
          command: "npx",
          args: ["-y", "some-server"],
        },
      ],
      secrets: {
        "my-token": {
          name: "My Token",
          provider: "keychain",
          purpose: "Auth",
        },
      },
    };

    const result = decodeExecutorFileConfig(raw);
    expect(result.sources).toHaveLength(4);
    expect(result.name).toBe("test");
    expect(result.secrets!["my-token"]!.name).toBe("My Token");
  });

  it("rejects invalid source kind", () => {
    const raw = {
      sources: [{ kind: "invalid", endpoint: "http://example.com" }],
    };
    expect(() => decodeExecutorFileConfig(raw)).toThrow();
  });
});

describe("loadConfig", () => {
  it.effect("returns null for missing file", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const result = yield* loadConfig(join(dir, "nonexistent.jsonc"));
        expect(result).toBeNull();
      }),
    ),
  );

  it.effect("loads a valid JSONC file with comments", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const fs = yield* FileSystem.FileSystem;
        const path = join(dir, "executor.jsonc");
        yield* fs.writeFileString(
          path,
          `{
  // This is a comment
  "name": "test",
  "sources": [
    {
      "kind": "openapi",
      "spec": "https://example.com/spec.json"
    }
  ]
}`,
        );

        const result = yield* loadConfig(path);
        expect(result).not.toBeNull();
        expect(result!.name).toBe("test");
        expect(result!.sources).toHaveLength(1);
      }),
    ),
  );

  it.effect("fails on invalid JSON", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const fs = yield* FileSystem.FileSystem;
        const path = join(dir, "executor.jsonc");
        yield* fs.writeFileString(path, "{ invalid json }");

        const result = yield* loadConfig(path).pipe(
          Effect.catchTag("ConfigParseError", (error) => Effect.succeed(error)),
        );
        expect(result).toBeInstanceOf(ConfigParseError);
      }),
    ),
  );
});

describe("write operations", () => {
  it.effect("addSourceToConfig creates file and adds source", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const fs = yield* FileSystem.FileSystem;
        const path = join(dir, "executor.jsonc");

        yield* addSourceToConfig(path, {
          kind: "openapi",
          spec: "https://example.com/spec.json",
          namespace: "example",
        });

        const content = yield* fs.readFileString(path);
        expect(content).toContain("openapi");
        expect(content).toContain("example");

        const config = yield* loadConfig(path);
        expect(config!.sources).toHaveLength(1);
        expect(config!.sources![0]!.kind).toBe("openapi");
      }),
    ),
  );

  it.effect("addSourceToConfig appends to existing sources", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const path = join(dir, "executor.jsonc");

        yield* addSourceToConfig(path, {
          kind: "openapi",
          spec: "https://example.com/spec.json",
          namespace: "first",
        });

        yield* addSourceToConfig(path, {
          kind: "graphql",
          endpoint: "https://example.com/graphql",
          namespace: "second",
        });

        const config = yield* loadConfig(path);
        expect(config!.sources).toHaveLength(2);
        expect(config!.sources![0]!.kind).toBe("openapi");
        expect(config!.sources![1]!.kind).toBe("graphql");
      }),
    ),
  );

  it.effect("addSourceToConfig preserves comments", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const fs = yield* FileSystem.FileSystem;
        const path = join(dir, "executor.jsonc");

        yield* fs.writeFileString(
          path,
          `{
  // My project config
  "name": "test",
  "sources": [
    {
      "kind": "openapi",
      "spec": "https://example.com/spec.json",
      "namespace": "first"
    }
  ]
}`,
        );

        yield* addSourceToConfig(path, {
          kind: "graphql",
          endpoint: "https://example.com/graphql",
          namespace: "second",
        });

        const content = yield* fs.readFileString(path);
        expect(content).toContain("// My project config");
        expect(content).toContain("graphql");
      }),
    ),
  );

  it.effect("addSourceToConfig replaces existing source with same namespace", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const path = join(dir, "executor.jsonc");

        yield* addSourceToConfig(path, {
          kind: "openapi",
          spec: "https://example.com/v1.json",
          namespace: "example",
        });

        // Add again with same namespace but different spec
        yield* addSourceToConfig(path, {
          kind: "openapi",
          spec: "https://example.com/v2.json",
          namespace: "example",
        });

        const config = yield* loadConfig(path);
        // Should have 1, not 2
        expect(config!.sources).toHaveLength(1);
        expect((config!.sources![0] as { spec: string }).spec).toBe("https://example.com/v2.json");
      }),
    ),
  );

  it.effect("removeSourceFromConfig removes by namespace", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const path = join(dir, "executor.jsonc");

        yield* writeConfig(path, {
          sources: [
            {
              kind: "openapi",
              spec: "https://example.com/spec.json",
              namespace: "keep",
            },
            {
              kind: "graphql",
              endpoint: "https://example.com/graphql",
              namespace: "remove-me",
            },
          ],
        });

        yield* removeSourceFromConfig(path, "remove-me");

        const config = yield* loadConfig(path);
        expect(config!.sources).toHaveLength(1);
        expect(config!.sources![0]!.kind).toBe("openapi");
      }),
    ),
  );

  it.effect("addSecretToConfig and removeSecretFromConfig", () =>
    withTmpDir((dir) =>
      Effect.gen(function* () {
        const path = join(dir, "executor.jsonc");

        yield* writeConfig(path, { sources: [] });

        yield* addSecretToConfig(path, "my-token", {
          name: "My Token",
          provider: "keychain",
        });

        let config = yield* loadConfig(path);
        expect(config!.secrets!["my-token"]!.name).toBe("My Token");

        yield* removeSecretFromConfig(path, "my-token");

        config = yield* loadConfig(path);
        expect(config!.secrets?.["my-token"]).toBeUndefined();
      }),
    ),
  );
});
