import { randomUUIDv7 } from "bun";
import { beforeEach, describe, expect, test } from "bun:test";
import { ConnectionType, createClient, ctx, isEnabled } from "../test-utils";

/**
 * Test suite for error handling, protocol failures, and edge cases
 * - Command errors (wrong arguments, invalid syntax)
 * - Protocol parsing failures
 * - Null/undefined/invalid input handling
 * - Type errors
 * - Edge cases
 */
describe.skipIf(!isEnabled)("Valkey: Error Handling", () => {
  beforeEach(() => {
    if (ctx.redis?.connected) {
      ctx.redis.close?.();
    }
    ctx.redis = createClient(ConnectionType.TCP);
  });
  describe("Command Errors", () => {
    test("should handle invalid command arguments", async () => {
      const client = ctx.redis;

      // Wrong number of arguments

      expect(async () => await client.send("SET", ["key"])).toThrowErrorMatchingInlineSnapshot(
        `"ERR wrong number of arguments for 'set' command"`,
      ); // Missing value argument
      expect(async () => await client.send("INVALID_COMMAND", ["a"])).toThrowErrorMatchingInlineSnapshot(
        `"ERR unknown command 'INVALID_COMMAND', with args beginning with: 'a' "`,
      ); // Invalid command
    });

    describe("should handle special character keys and values", async () => {
      // Keys with special characters
      const specialKeys = [
        "key with spaces",
        "key\nwith\nnewlines",
        "key\twith\ttabs",
        "key:with:colons",
        "key-with-unicode-♥-❤-★",
      ];

      // Values with special characters
      const specialValues = [
        "value with spaces",
        "value\nwith\nnewlines",
        "value\twith\ttabs",
        "value:with:colons",
        "value-with-unicode-♥-❤-★",
        // RESP protocol special characters
        "+OK\r\n",
        "-ERR\r\n",
        "$5\r\nhello\r\n",
        "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",
      ];

      for (const key of specialKeys) {
        for (const value of specialValues) {
          const testKey = `special-key-${randomUUIDv7()}`;
          test(`should handle special characters in key "${key}" and value "${value}"`, async () => {
            const client = ctx.redis;
            // Set and get should work with special characters
            await client.set(testKey, value);
            const result = await client.get(testKey);
            expect(result).toBe(value);
          });
        }
      }
    });
  });

  describe("Null/Undefined/Invalid Input Handling", () => {
    test.only("should handle undefined/null command arguments", async () => {
      const client = ctx.redis;

      // undefined key
      // @ts-expect-error: Testing runtime behavior with invalid types
      expect(async () => await client.get(undefined)).toThrowErrorMatchingInlineSnapshot(
        `"Expected key to be a string or buffer for 'get'."`,
      );

      // null key
      // @ts-expect-error: Testing runtime behavior with invalid types
      expect(async () => await client.get(null)).toThrowErrorMatchingInlineSnapshot(
        `"Expected key to be a string or buffer for 'get'."`,
      );

      // undefined value
      // @ts-expect-error: Testing runtime behavior with invalid types
      expect(async () => await client.set("valid-key", undefined)).toThrowErrorMatchingInlineSnapshot(
        `"Expected value to be a string or buffer or number for 'set'."`,
      );

      expect(async () => await client.set("valid-key", null)).toThrowErrorMatchingInlineSnapshot(
        `"Expected value to be a string or buffer or number for 'set'."`,
      );
    });

    test("should handle invalid sendCommand inputs", async () => {
      const client = ctx.redis;

      // Undefined command
      // @ts-expect-error: Testing runtime behavior with invalid types
      expect(async () => await client.send(undefined, [])).toThrowErrorMatchingInlineSnapshot(
        `"ERR unknown command 'undefined', with args beginning with: "`,
      );

      // Invalid args type
      // @ts-expect-error: Testing runtime behavior with invalid types
      expect(async () => await client.send("GET", "not-an-array")).toThrowErrorMatchingInlineSnapshot(
        `"Arguments must be an array"`,
      );

      // Non-string command
      // @ts-expect-error: Testing runtime behavior with invalid types
      expect(async () => await client.send(123, [])).toThrowErrorMatchingInlineSnapshot(
        `"ERR unknown command '123', with args beginning with: "`,
      );
    });
  });

  describe("Protocol and Parser Edge Cases", () => {
    test("should handle various data types correctly", async () => {
      const client = ctx.redis;

      // Integer/string conversions
      await client.set("int-key", "42");

      // INCR should return as number
      const incrResult = await client.incr("int-key");
      expect(typeof incrResult).toBe("number");
      expect(incrResult).toBe(43);

      // GET should return as string
      const getResult = await client.get("int-key");
      expect(typeof getResult).toBe("string");
      expect(getResult).toBe("43");

      // Boolean handling for EXISTS command
      await client.set("exists-key", "value");
      const existsResult = await client.exists("exists-key");
      expect(typeof existsResult).toBe("boolean");
      expect(existsResult).toBe(true);

      const notExistsResult = await client.exists("not-exists-key");
      expect(typeof notExistsResult).toBe("boolean");
      expect(notExistsResult).toBe(false);

      // Null handling for non-existent keys
      const nullResult = await client.get("not-exists-key");
      expect(nullResult).toBeNull();
    });

    test("should handle complex RESP3 types", async () => {
      const client = ctx.redis;

      // HGETALL returns object in RESP3
      const hashKey = `hash-${randomUUIDv7()}`;
      await client.send("HSET", [hashKey, "field1", "value1", "field2", "value2"]);

      const hashResult = await client.send("HGETALL", [hashKey]);

      // Hash results should be objects in RESP3
      expect(typeof hashResult).toBe("object");
      expect(hashResult).not.toBeNull();

      if (hashResult !== null) {
        expect(hashResult.field1).toBe("value1");
        expect(hashResult.field2).toBe("value2");
      }

      // Error type handling
      expect(async () => await client.send("HGET", [])).toThrowErrorMatchingInlineSnapshot(
        `"ERR wrong number of arguments for 'hget' command"`,
      ); // Missing key and field

      // NULL handling from various commands
      const nullResult = await client.send("HGET", [hashKey, "nonexistent"]);
      expect(nullResult).toBeNull();
    });

    test("should handle RESP protocol boundaries", async () => {
      const client = ctx.redis;

      // Mix of command types to stress protocol parser
      const commands = [
        client.set("key1", "value1"),
        client.get("key1"),
        client.send("PING", []),
        client.incr("counter"),
        client.exists("key1"),
        client.send("HSET", ["hash", "field", "value"]),
        client.send("HGETALL", ["hash"]),
        client.set("key2", "x".repeat(1000)), // Larger value
        client.get("key2"),
      ];

      // Run all commands in parallel to stress protocol handling
      await Promise.all(commands);

      // Verify data integrity

      expect(await client.get("key1")).toBe("value1");
      expect(await client.exists("key1")).toBe(true);
      expect(await client.get("key2")).toBe("x".repeat(1000));
      expect(await client.send("HGET", ["hash", "field"])).toBe("value");
    });
  });

  describe("Resource Management and Edge Cases", () => {
    test("should handle very large number of parallel commands", async () => {
      const client = ctx.redis;

      // Create a large number of parallel commands
      const parallelCount = 1000;
      const commands = [];

      for (let i = 0; i < parallelCount; i++) {
        const key = `parallel-key-${i}`;
        commands.push(client.set(key, `value-${i}`));
      }

      // Execute all in parallel
      await Promise.all(commands);

      // Verify some random results
      for (let i = 0; i < 10; i++) {
        const index = Math.floor(Math.random() * parallelCount);
        const key = `parallel-key-${index}`;
        const value = await client.get(key);
        expect(value).toBe(`value-${index}`);
      }
    });

    test("should handle many rapid sequential commands", async () => {
      const client = ctx.redis;

      // Create many sequential commands
      const sequentialCount = 500;

      for (let i = 0; i < sequentialCount; i++) {
        const key = `sequential-key-${i}`;
        await client.set(key, `value-${i}`);

        // Periodically verify to ensure integrity
        if (i % 50 === 0) {
          const value = await client.get(key);
          expect(value).toBe(`value-${i}`);
        }
      }
    });

    test("should handle command after disconnect and reconnect", async () => {
      const client = ctx.redis;

      // Set initial value
      const key = `reconnect-key-${randomUUIDv7()}`;
      await client.set(key, "initial-value");

      // Disconnect explicitly
      client.close();

      // This command should fail
      expect(async () => await client.get(key)).toThrowErrorMatchingInlineSnapshot(`"Connection has failed"`);
    });

    test("should handle binary data", async () => {
      // Binary data in both keys and values
      const client = ctx.redis;

      // Create Uint8Array with binary data
      const binaryData = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]);
      const binaryString = String.fromCharCode(...binaryData);

      await client.set("binary-key", binaryString);

      // Get it back
      const result = await client.get("binary-key");

      // Compare binary data
      expect(result).toBe(binaryString);
    });
  });
});
