import { describe, it, expect } from "vitest";
import { Hashing } from "../../../components/experiments/designer/state/hashing";
import type {
  ExperimentStep,
  ExperimentAction,
} from "../../experiment-designer/types";

describe("Hashing Utilities", () => {
  describe("canonicalize", () => {
    it("should sort object keys", () => {
      const obj1 = { b: 2, a: 1 };
      const obj2 = { a: 1, b: 2 };
      expect(JSON.stringify(Hashing.canonicalize(obj1))).toBe(
        JSON.stringify(Hashing.canonicalize(obj2)),
      );
    });

    it("should remove undefined values", () => {
      const obj = { a: 1, b: undefined, c: null };
      const canonical = Hashing.canonicalize(obj) as any;
      expect(canonical).toHaveProperty("a");
      expect(canonical).toHaveProperty("c"); // null is preserved
      expect(canonical).not.toHaveProperty("b");
    });

    it("should preserve array order", () => {
      const arr = [3, 1, 2];
      const canonical = Hashing.canonicalize(arr);
      expect(canonical).toEqual([3, 1, 2]);
    });
  });

  describe("computeDesignHash", () => {
    const step1: ExperimentStep = {
      id: "step-1",
      name: "Step 1",
      type: "sequential",
      order: 0,
      trigger: { type: "trial_start", conditions: {} },
      actions: [],
      expanded: true,
    };

    const step2: ExperimentStep = {
      id: "step-2",
      name: "Step 2",
      type: "sequential",
      order: 1,
      trigger: { type: "previous_step", conditions: {} },
      actions: [],
      expanded: true,
    };

    it("should produce deterministic hash regardless of input array order", async () => {
      const hash1 = await Hashing.computeDesignHash([step1, step2]);
      const hash2 = await Hashing.computeDesignHash([step2, step1]);
      expect(hash1).toBe(hash2);
    });

    it("should change hash when step content changes", async () => {
      const hash1 = await Hashing.computeDesignHash([step1]);
      const modifiedStep = { ...step1, name: "Modified Name" };
      const hash2 = await Hashing.computeDesignHash([modifiedStep]);
      expect(hash1).not.toBe(hash2);
    });

    it("should change hash when parameters change if included", async () => {
      const action: ExperimentAction = {
        id: "act-1",
        type: "log",
        name: "Log",
        category: "observation",
        parameters: { message: "A" },
        source: { kind: "core", baseActionId: "log" },
        execution: { transport: "internal" },
      };
      const stepWithAction = { ...step1, actions: [action] };

      const hash1 = await Hashing.computeDesignHash([stepWithAction], {
        includeParameterValues: true,
      });

      const modifiedAction = { ...action, parameters: { message: "B" } };
      const stepModified = { ...step1, actions: [modifiedAction] };

      const hash2 = await Hashing.computeDesignHash([stepModified], {
        includeParameterValues: true,
      });

      expect(hash1).not.toBe(hash2);
    });

    it("should NOT change hash when parameters change if excluded", async () => {
      const action: ExperimentAction = {
        id: "act-1",
        type: "log",
        name: "Log",
        category: "observation",
        parameters: { message: "A" },
        source: { kind: "core", baseActionId: "log" },
        execution: { transport: "internal" },
      };
      const stepWithAction = { ...step1, actions: [action] };

      const hash1 = await Hashing.computeDesignHash([stepWithAction], {
        includeParameterValues: false,
      });

      const modifiedAction = { ...action, parameters: { message: "B" } };
      const stepModified = { ...step1, actions: [modifiedAction] };

      const hash2 = await Hashing.computeDesignHash([stepModified], {
        includeParameterValues: false,
      });

      expect(hash1).toBe(hash2);
    });
  });
});
