import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Path from "effect/Path"

const testLayer = NodeServices.layer

describe("OpenApiPatch", () => {
  describe("parsePatchInput", () => {
    describe("inline JSON", () => {
      it.effect("parses valid inline JSON patch", () =>
        Effect.gen(function*() {
          const result = yield* OpenApiPatch.parsePatchInput(
            "[{\"op\":\"add\",\"path\":\"/foo\",\"value\":\"bar\"}]"
          )
          assert.deepStrictEqual(result, [{ op: "add", path: "/foo", value: "bar" }])
        }).pipe(Effect.provide(testLayer)))

      it.effect("parses inline JSON with multiple operations", () =>
        Effect.gen(function*() {
          const result = yield* OpenApiPatch.parsePatchInput(
            "[{\"op\":\"add\",\"path\":\"/a\",\"value\":1},{\"op\":\"remove\",\"path\":\"/b\"},{\"op\":\"replace\",\"path\":\"/c\",\"value\":true}]"
          )
          assert.strictEqual(result.length, 3)
          assert.strictEqual(result[0].op, "add")
          assert.strictEqual(result[1].op, "remove")
          assert.strictEqual(result[2].op, "replace")
        }).pipe(Effect.provide(testLayer)))

      it.effect("fails on invalid JSON syntax", () =>
        Effect.gen(function*() {
          const exit = yield* Effect.exit(
            OpenApiPatch.parsePatchInput("[{\"op\":\"add\" \"path\":\"/foo\"}]")
          )
          assert.isTrue(Exit.isFailure(exit))
        }).pipe(Effect.provide(testLayer)))

      it.effect("fails on unsupported operation", () =>
        Effect.gen(function*() {
          const exit = yield* Effect.exit(
            OpenApiPatch.parsePatchInput("[{\"op\":\"copy\",\"from\":\"/a\",\"path\":\"/b\"}]")
          )
          assert.isTrue(Exit.isFailure(exit))
        }).pipe(Effect.provide(testLayer)))

      it.effect("fails on missing path field", () =>
        Effect.gen(function*() {
          const exit = yield* Effect.exit(
            OpenApiPatch.parsePatchInput("[{\"op\":\"add\",\"value\":\"test\"}]")
          )
          assert.isTrue(Exit.isFailure(exit))
        }).pipe(Effect.provide(testLayer)))

      it.effect("fails on missing value for add operation", () =>
        Effect.gen(function*() {
          const exit = yield* Effect.exit(
            OpenApiPatch.parsePatchInput("[{\"op\":\"add\",\"path\":\"/foo\"}]")
          )
          assert.isTrue(Exit.isFailure(exit))
        }).pipe(Effect.provide(testLayer)))

      it.effect("allows missing value for remove operation", () =>
        Effect.gen(function*() {
          const result = yield* OpenApiPatch.parsePatchInput(
            "[{\"op\":\"remove\",\"path\":\"/foo\"}]"
          )
          assert.deepStrictEqual(result, [{ op: "remove", path: "/foo" }])
        }).pipe(Effect.provide(testLayer)))
    })

    describe("file paths", () => {
      it.effect("parses valid JSON file", () =>
        Effect.gen(function*() {
          const pathService = yield* Path.Path
          const filePath = pathService.join(
            import.meta.dirname,
            "fixtures/patches/valid-add.json"
          )
          const result = yield* OpenApiPatch.parsePatchInput(filePath)
          assert.strictEqual(result.length, 1)
          assert.strictEqual(result[0].op, "add")
          assert.strictEqual(result[0].path, "/info/x-custom")
        }).pipe(Effect.provide(testLayer)))

      it.effect("parses valid YAML file", () =>
        Effect.gen(function*() {
          const pathService = yield* Path.Path
          const filePath = pathService.join(
            import.meta.dirname,
            "fixtures/patches/valid-patch.yaml"
          )
          const result = yield* OpenApiPatch.parsePatchInput(filePath)
          assert.strictEqual(result.length, 2)
          assert.strictEqual(result[0].op, "replace")
          assert.strictEqual(result[1].op, "add")
        }).pipe(Effect.provide(testLayer)))

      it.effect("parses multiple operations from JSON file", () =>
        Effect.gen(function*() {
          const pathService = yield* Path.Path
          const filePath = pathService.join(
            import.meta.dirname,
            "fixtures/patches/valid-multiple.json"
          )
          const result = yield* OpenApiPatch.parsePatchInput(filePath)
          assert.strictEqual(result.length, 3)
        }).pipe(Effect.provide(testLayer)))

      it.effect("fails on file with unsupported operation", () =>
        Effect.gen(function*() {
          const pathService = yield* Path.Path
          const filePath = pathService.join(
            import.meta.dirname,
            "fixtures/patches/invalid-op.json"
          )
          const exit = yield* Effect.exit(OpenApiPatch.parsePatchInput(filePath))
          assert.isTrue(Exit.isFailure(exit))
        }).pipe(Effect.provide(testLayer)))

      it.effect("fails on file with missing path", () =>
        Effect.gen(function*() {
          const pathService = yield* Path.Path
          const filePath = pathService.join(
            import.meta.dirname,
            "fixtures/patches/missing-path.json"
          )
          const exit = yield* Effect.exit(OpenApiPatch.parsePatchInput(filePath))
          assert.isTrue(Exit.isFailure(exit))
        }).pipe(Effect.provide(testLayer)))

      it.effect("falls back to inline JSON when file does not exist", () =>
        Effect.gen(function*() {
          const result = yield* OpenApiPatch.parsePatchInput(
            "[{\"op\":\"add\",\"path\":\"/x\",\"value\":1}]"
          )
          assert.deepStrictEqual(result, [{ op: "add", path: "/x", value: 1 }])
        }).pipe(Effect.provide(testLayer)))
    })
  })

  describe("applyPatches", () => {
    it.effect("applies single add patch", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" } }
        const patches = [{
          source: "test",
          patch: [{ op: "add" as const, path: "/info/version", value: "1.0.0" }]
        }]
        const result = yield* OpenApiPatch.applyPatches(patches, document)
        assert.deepStrictEqual(result, { info: { title: "Test", version: "1.0.0" } })
      }))

    it.effect("applies single remove patch", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test", deprecated: true } }
        const patches = [{
          source: "test",
          patch: [{ op: "remove" as const, path: "/info/deprecated" }]
        }]
        const result = yield* OpenApiPatch.applyPatches(patches, document)
        assert.deepStrictEqual(result, { info: { title: "Test" } })
      }))

    it.effect("applies single replace patch", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Old Title" } }
        const patches = [{
          source: "test",
          patch: [{ op: "replace" as const, path: "/info/title", value: "New Title" }]
        }]
        const result = yield* OpenApiPatch.applyPatches(patches, document)
        assert.deepStrictEqual(result, { info: { title: "New Title" } })
      }))

    it.effect("applies multiple patches in sequence", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Original", version: "0.0.1" } }
        const patches = [
          {
            source: "patch1",
            patch: [{ op: "replace" as const, path: "/info/title", value: "Step 1" }]
          },
          {
            source: "patch2",
            patch: [{ op: "replace" as const, path: "/info/version", value: "1.0.0" }]
          },
          {
            source: "patch3",
            patch: [{ op: "add" as const, path: "/info/x-patched", value: true }]
          }
        ]
        const result = yield* OpenApiPatch.applyPatches(patches, document)
        assert.deepStrictEqual(result, {
          info: { title: "Step 1", version: "1.0.0", "x-patched": true }
        })
      }))

    it.effect("applies multiple operations within a single patch", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" }, paths: {} }
        const patches = [{
          source: "test",
          patch: [
            { op: "replace" as const, path: "/info/title", value: "Updated" },
            { op: "add" as const, path: "/info/version", value: "2.0.0" }
          ]
        }]
        const result = yield* OpenApiPatch.applyPatches(patches, document)
        assert.deepStrictEqual(result, {
          info: { title: "Updated", version: "2.0.0" },
          paths: {}
        })
      }))

    it.effect("fails when path does not exist for replace", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" } }
        const patches = [{
          source: "test",
          patch: [{ op: "replace" as const, path: "/info/nonexistent", value: "x" }]
        }]
        const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
        assert.isTrue(Exit.isFailure(exit))
      }))

    it.effect("fails when path does not exist for remove", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" } }
        const patches = [{
          source: "test",
          patch: [{ op: "remove" as const, path: "/info/nonexistent" }]
        }]
        const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
        assert.isTrue(Exit.isFailure(exit))
      }))

    it.effect("accumulates multiple errors", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" } }
        const patches = [{
          source: "test.json",
          patch: [
            { op: "replace" as const, path: "/info/nonexistent1", value: "x" },
            { op: "remove" as const, path: "/info/nonexistent2" },
            { op: "replace" as const, path: "/info/nonexistent3", value: "y" }
          ]
        }]
        const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
        assert.isTrue(Exit.isFailure(exit))
        if (Exit.isFailure(exit)) {
          const failure = exit.cause.reasons[0]
          if (failure._tag === "Fail") {
            assert.strictEqual(failure.error._tag, "JsonPatchAggregateError")
            assert.strictEqual(failure.error.errors.length, 3)
            assert.include(failure.error.message, "3 patch operations failed")
            assert.include(failure.error.message, "/info/nonexistent1")
            assert.include(failure.error.message, "/info/nonexistent2")
            assert.include(failure.error.message, "/info/nonexistent3")
          }
        }
      }))

    it.effect("accumulates errors across multiple patches", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" } }
        const patches = [
          {
            source: "patch1.json",
            patch: [{ op: "remove" as const, path: "/info/missing1" }]
          },
          {
            source: "patch2.json",
            patch: [{ op: "remove" as const, path: "/info/missing2" }]
          }
        ]
        const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
        assert.isTrue(Exit.isFailure(exit))
        if (Exit.isFailure(exit)) {
          const failure = exit.cause.reasons[0]
          if (failure._tag === "Fail") {
            assert.strictEqual(failure.error.errors.length, 2)
            assert.include(failure.error.message, "patch1.json")
            assert.include(failure.error.message, "patch2.json")
          }
        }
      }))

    it.effect("preserves unmodified parts of document", () =>
      Effect.gen(function*() {
        const document = {
          info: { title: "Test", description: "Unchanged" },
          paths: { "/users": { get: {} } },
          components: { schemas: {} }
        }
        const patches = [{
          source: "test",
          patch: [{ op: "replace" as const, path: "/info/title", value: "Changed" }]
        }]
        const result = yield* OpenApiPatch.applyPatches(patches, document)
        assert.strictEqual((result as { info: { description: string } }).info.description, "Unchanged")
        assert.deepStrictEqual((result as { paths: object }).paths, { "/users": { get: {} } })
        assert.deepStrictEqual((result as { components: object }).components, { schemas: {} })
      }))

    it.effect("returns original document when no patches provided", () =>
      Effect.gen(function*() {
        const document = { info: { title: "Test" } }
        const result = yield* OpenApiPatch.applyPatches([], document)
        assert.deepStrictEqual(result, document)
      }))
  })

  describe("error messages", () => {
    it.effect("JsonPatchParseError has descriptive message", () =>
      Effect.gen(function*() {
        const error = new OpenApiPatch.JsonPatchParseError({
          source: "./fix.json",
          reason: "Unexpected token"
        })
        assert.strictEqual(
          error.message,
          "Failed to parse patch from ./fix.json: Unexpected token"
        )
      }))

    it.effect("JsonPatchValidationError has descriptive message", () =>
      Effect.gen(function*() {
        const error = new OpenApiPatch.JsonPatchValidationError({
          source: "inline",
          reason: "Missing 'path' field"
        })
        assert.strictEqual(
          error.message,
          "Invalid JSON Patch from inline: Missing 'path' field"
        )
      }))

    it.effect("JsonPatchApplicationError has descriptive message", () =>
      Effect.gen(function*() {
        const error = new OpenApiPatch.JsonPatchApplicationError({
          source: "./fix.json",
          operationIndex: 2,
          operation: "remove",
          path: "/info/x",
          reason: "Property does not exist"
        })
        assert.strictEqual(
          error.message,
          "Failed to apply patch from ./fix.json: operation 2 (remove at /info/x): Property does not exist"
        )
      }))

    it.effect("JsonPatchAggregateError has descriptive message", () =>
      Effect.gen(function*() {
        const error = new OpenApiPatch.JsonPatchAggregateError({
          errors: [
            new OpenApiPatch.JsonPatchApplicationError({
              source: "./fix.json",
              operationIndex: 0,
              operation: "replace",
              path: "/info/x",
              reason: "Property does not exist"
            }),
            new OpenApiPatch.JsonPatchApplicationError({
              source: "./other.json",
              operationIndex: 1,
              operation: "remove",
              path: "/paths/~1users",
              reason: "Path not found"
            })
          ]
        })
        assert.include(error.message, "2 patch operations failed")
        assert.include(error.message, "1. [./fix.json] op 0 (replace at /info/x)")
        assert.include(error.message, "2. [./other.json] op 1 (remove at /paths/~1users)")
      }))
  })
})
