// frontend/src/lib/models/__tests__/MigrationModel.test.ts
// #region MigrationModelTests [C:3] [TYPE Module] [SEMANTICS test,model,migration]
// @BRIEF L1 unit tests for MigrationModel @INVARIANT guarantees — no DOM render.
// @RELATION BINDS_TO -> [MigrationModel]
// @TEST_INVARIANT: source-env-resets-dashboards -> VERIFIED_BY: [test_selectSourceEnv_resets_dashboards]
// @TEST_INVARIANT: source-env-resets-databases -> VERIFIED_BY: [test_selectSourceEnv_resets_databases]
// @TEST_INVARIANT: source-env-clears-dryrun -> VERIFIED_BY: [test_selectSourceEnv_clears_dryRunResult]
// @TEST_INVARIANT: migration-blocked-same-env -> VERIFIED_BY: [test_executeMigration_blocked_same_env]
// @TEST_INVARIANT: migration-blocked-no-dashboards -> VERIFIED_BY: [test_executeMigration_blocked_no_dashboards]
// @TEST_INVARIANT: dryrun-required-before-execute -> VERIFIED_BY: [test_canExecute_false_without_dryRunResult]
import { describe, it, expect, vi, beforeEach } from "vitest";

// Mock the API module before importing
vi.mock("$lib/api.js", () => ({
  api: {
    requestApi: vi.fn(),
    postApi: vi.fn(),
    getTask: vi.fn().mockResolvedValue({ id: "task-1", status: "RUNNING" }),
    getEnvironmentsList: vi.fn().mockResolvedValue([
      { id: "env-1", name: "Dev" },
      { id: "env-2", name: "Prod" },
    ]),
  },
}));

vi.mock("$lib/stores.svelte.js", () => ({
	selectedTask: { set: vi.fn(), current: null, subscribe: vi.fn() },
}));

vi.mock("$lib/stores/environmentContext.svelte.js", () => ({
	environmentContextStore: { current: { selectedEnvId: "env-1" } },
}));

vi.mock("../../services/taskService.js", () => ({
  resumeTask: vi.fn().mockResolvedValue({ ok: true }),
}));

vi.mock('$lib/i18n/index.svelte.js', () => ({
  t: {
    migration: {
      select_both_envs: "Select both environments",
      different_envs: "Must be different",
      select_dashboards: "Select dashboards",
      resume_failed: "Resume failed",
    },
  },
}));

import { MigrationModel } from "../MigrationModel.svelte.ts";
import { api } from "$lib/api.js";
import { selectedTask } from "$lib/stores.svelte.js";

describe("MigrationModel — L1 invariants (no render)", () => {
  let model: MigrationModel;

  beforeEach(() => {
    vi.clearAllMocks();
    model = new MigrationModel();
    selectedTask.current = null;
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Changing source environment resets dashboard selection
  // ═══════════════════════════════════════════════════════════════
  describe("sourceEnvId change → reset invariants", () => {
    it("resets selectedDashboardIds when source env changes", () => {
      model.selectedDashboardIds = ["1", "2", "3"];
      model.selectSourceEnv("env-2");
      expect(model.selectedDashboardIds).toEqual([]);
    });

    it("resets dashboards array when source env changes", () => {
      model.dashboards = [{ id: "1" }, { id: "2" }];
      model.selectedDashboardIds = ["1"];
      model.selectSourceEnv("env-2");
      // dashboards will be cleared and then refetched async — but sync reset is immediate
      expect(model.selectedDashboardIds).toEqual([]);
    });

    it("does NOT reset if same env is selected again", () => {
      model.sourceEnvId = "env-1";
      model.selectedDashboardIds = ["1", "2", "3"];
      model.selectSourceEnv("env-1");
      expect(model.selectedDashboardIds).toEqual(["1", "2", "3"]);
    });

    // @INVARIANT: Changing source environment resets databases, mappings, and suggestions
    it("resets databases, mappings, and suggestions", () => {
      model.sourceDatabases = [{ uuid: "db-1", database_name: "test" }];
      model.targetDatabases = [{ uuid: "db-2", database_name: "test" }];
      model.mappings = [{ id: 1 }];
      model.suggestions = [{ id: 2 }];
      model.selectSourceEnv("env-2");
      expect(model.sourceDatabases).toEqual([]);
      expect(model.targetDatabases).toEqual([]);
      expect(model.mappings).toEqual([]);
      expect(model.suggestions).toEqual([]);
    });

    // @INVARIANT: Changing source environment clears dry-run result
    it("clears dryRunResult when source env changes", () => {
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      model.selectSourceEnv("env-2");
      expect(model.dryRunResult).toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Migration blocked unless source≠target
  // ═══════════════════════════════════════════════════════════════
  describe("executeMigration blocked invariants", () => {
    it("blocks when source and target are the same", async () => {
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-1";
      model.selectedDashboardIds = ["1"];
      await model.executeMigration();
      expect(model.error).toContain("different");
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("blocks when no dashboards selected", async () => {
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-2";
      model.selectedDashboardIds = [];
      await model.executeMigration();
      expect(model.error).toContain("dashboards");
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("blocks when source env is empty", async () => {
      model.sourceEnvId = "";
      model.targetEnvId = "env-2";
      model.selectedDashboardIds = ["1"];
      await model.executeMigration();
      expect(model.error).toContain("both");
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("blocks when target env is empty", async () => {
      model.sourceEnvId = "env-1";
      model.targetEnvId = "";
      model.selectedDashboardIds = ["1"];
      await model.executeMigration();
      expect(model.error).toContain("both");
      expect(api.postApi).not.toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: canExecute is false without dryRunResult
  // ═══════════════════════════════════════════════════════════════
  describe("canExecute derived", () => {
    it("false when dryRunResult is null", () => {
      model.selectedDashboardIds = ["1"];
      model.dryRunResult = null;
      expect(model.canExecute).toBe(false);
    });

    it("false when no dashboards selected", () => {
      model.selectedDashboardIds = [];
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      expect(model.canExecute).toBe(false);
    });

    it("true when dryRunResult exists AND dashboards selected", () => {
      model.selectedDashboardIds = ["1"];
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      expect(model.canExecute).toBe(true);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: stepReady derived correctness
  // ═══════════════════════════════════════════════════════════════
  describe("stepReady derived", () => {
    it("step 1 ready when both envs selected and distinct", () => {
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-2";
      expect(model.stepReady[1]).toBe(true);
    });

    it("step 1 NOT ready when envs are same", () => {
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-1";
      expect(model.stepReady[1]).toBe(false);
    });

    it("step 1 NOT ready when source is empty", () => {
      model.sourceEnvId = "";
      model.targetEnvId = "env-2";
      expect(model.stepReady[1]).toBe(false);
    });

    it("step 2 ready when dashboards selected", () => {
      model.selectedDashboardIds = ["1"];
      expect(model.stepReady[2]).toBe(true);
    });

    it("step 2 NOT ready when no dashboards", () => {
      model.selectedDashboardIds = [];
      expect(model.stepReady[2]).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Dashboard toggle idempotent
  // ═══════════════════════════════════════════════════════════════
  describe("toggleDashboard", () => {
    it("adds dashboard id when not present", () => {
      model.toggleDashboard("5");
      expect(model.selectedDashboardIds).toContain("5");
    });

    it("removes dashboard id when already present", () => {
      model.selectedDashboardIds = ["1", "5", "3"];
      model.toggleDashboard("5");
      expect(model.selectedDashboardIds).toEqual(["1", "3"]);
    });

    it("clears dryRunResult on toggle", () => {
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      model.toggleDashboard("1");
      expect(model.dryRunResult).toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: calculateDryRun → toggleDashboard clears dryRunResult
  // ═══════════════════════════════════════════════════════════════
  describe("calculateDryRun then toggleDashboard invariant", () => {
    it("clears dryRunResult when toggleDashboard is called after dry-run", async () => {
      // Arrange: satisfy preconditions and mock a successful dry-run API
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-2";
      model.selectedDashboardIds = ["1", "2"];
      const dryRunResponse = {
        summary: { dashboards: { create: 1, update: 0, delete: 0 }, charts: {}, datasets: {} },
        risk: { score: 10, level: "low", items: [] },
        selected_dashboard_titles: ["Sales"],
      };
      vi.mocked(api.postApi).mockResolvedValueOnce(dryRunResponse);

      // Act: run dry-run
      await model.calculateDryRun();
      expect(model.dryRunResult).toEqual(dryRunResponse);

      // Act: toggle a dashboard selection
      model.toggleDashboard("1");

      // Assert: dry-run result is cleared
      expect(model.dryRunResult).toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: setReplaceDb resets databases when toggling OFF
  // ═══════════════════════════════════════════════════════════════
  describe("setReplaceDb", () => {
    it("clears databases and mappings when set to false", () => {
      model.sourceDatabases = [{ uuid: "db-1", database_name: "test" }];
      model.mappings = [{ id: 1 }];
      model.setReplaceDb(false);
      expect(model.sourceDatabases).toEqual([]);
      expect(model.mappings).toEqual([]);
    });

    it("clears dryRunResult", () => {
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      model.setReplaceDb(true);
      expect(model.dryRunResult).toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: goToStep gates by readiness
  // ═══════════════════════════════════════════════════════════════
  describe("goToStep gating", () => {
    it("allows backward navigation (step <= currentStep)", () => {
      model.currentStep = 3;
      model.goToStep(1);
      expect(model.currentStep).toBe(1);
    });

    it("allows forward to step 2 when step 1 is ready", () => {
      model.currentStep = 1;
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-2";
      model.goToStep(2);
      expect(model.currentStep).toBe(2);
    });

    it("blocks forward to step 2 when step 1 is not ready", () => {
      model.currentStep = 1;
      model.sourceEnvId = "";
      model.targetEnvId = "";
      model.goToStep(2);
      expect(model.currentStep).toBe(1); // unchanged
    });

    it("allows forward to step 4 only when dryRunResult exists", () => {
      model.currentStep = 3;
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      model.selectedDashboardIds = ["1"];
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-2";
      model.goToStep(4);
      expect(model.currentStep).toBe(4);
    });

    it("blocks forward to step 4 when dryRunResult is null", () => {
      model.currentStep = 3;
      model.dryRunResult = null;
      model.goToStep(4);
      expect(model.currentStep).toBe(3); // unchanged
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Password prompt detection
  // ═══════════════════════════════════════════════════════════════
  describe("checkPasswordPrompt", () => {
    it("shows password prompt when task is AWAITING_INPUT with database_password", () => {
      selectedTask.current = {
        id: "task-1",
        status: "AWAITING_INPUT",
        input_request: {
          type: "database_password",
          databases: ["db1", "db2"],
          error_message: "Auth failed",
        },
      };
      model.checkPasswordPrompt();
      expect(model.showPasswordPrompt).toBe(true);
      expect(model.passwordPromptDatabases).toEqual(["db1", "db2"]);
      expect(model.passwordPromptErrorMessage).toBe("Auth failed");
    });

    it("does NOT show prompt when task is not AWAITING_INPUT", () => {
      selectedTask.current = { id: "task-1", status: "RUNNING" };
      model.checkPasswordPrompt();
      expect(model.showPasswordPrompt).toBe(false);
    });

    it("does NOT show prompt when input_request type is different", () => {
      selectedTask.current = {
        id: "task-1",
        status: "AWAITING_INPUT",
        input_request: { type: "confirmation" },
      };
      model.checkPasswordPrompt();
      expect(model.showPasswordPrompt).toBe(false);
    });

    it("does NOT show prompt when activeTask is null", () => {
      selectedTask.current = null;
      model.checkPasswordPrompt();
      expect(model.showPasswordPrompt).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: clearError resets error string
  // ═══════════════════════════════════════════════════════════════
  describe("clearError", () => {
    it("clears error", () => {
      model.error = "Something went wrong";
      model.clearError();
      expect(model.error).toBe("");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: screenState derived correctness
  // ═══════════════════════════════════════════════════════════════
  describe("screenState derived", () => {
    it("idle when loading", () => {
      model.loading = true;
      expect(model.screenState).toBe("idle");
    });

    it("error when error is set", () => {
      model.loading = false;
      model.error = "Fail";
      expect(model.screenState).toBe("error");
    });

    it("loading when dryRunLoading", () => {
      model.loading = false;
      model.error = "";
      model.dryRunLoading = true;
      expect(model.screenState).toBe("loading");
    });

    it("review when dryRunResult exists", () => {
      model.loading = false;
      model.error = "";
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      expect(model.screenState).toBe("review");
    });

    it("ready when no special state active", () => {
      model.loading = false;
      model.error = "";
      model.dryRunResult = null;
      model.currentStep = 1;
      expect(model.screenState).toBe("ready");
    });

    it("executing when currentStep >= 4", () => {
      model.loading = false;
      model.error = "";
      model.dryRunResult = null;
      model.currentStep = 4;
      expect(model.screenState).toBe("executing");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loadEnvironments pre-fills source from context
  // ═══════════════════════════════════════════════════════════════
  describe("loadEnvironments", () => {
    it("sets loading true then false after fetch", async () => {
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce([]);
      const promise = model.loadEnvironments();
      expect(model.loading).toBe(true);
      await promise;
      expect(model.loading).toBe(false);
    });

    it("pre-fills sourceEnvId from active context", async () => {
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce([
        { id: "env-1", name: "Dev" },
        { id: "env-2", name: "Prod" },
      ]);
      // envContextStore.current.selectedEnvId = "env-1" (from mock above)
      await model.loadEnvironments();
      expect(model.sourceEnvId).toBe("env-1");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: _buildSelection produces correct payload
  // ═══════════════════════════════════════════════════════════════
  describe("_buildSelection (private)", () => {
    it("builds correct DashboardSelection payload", () => {
      model.selectedDashboardIds = ["1", "2"];
      model.sourceEnvId = "src";
      model.targetEnvId = "tgt";
      model.replaceDb = true;
      model.fixCrossFilters = false;
      const sel = model._buildSelection();
      expect(sel).toEqual({
        selected_ids: ["1", "2"],
        source_env_id: "src",
        target_env_id: "tgt",
        replace_db_config: true,
        fix_cross_filters: false,
      });
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // executeMigration with custom endpoint
  // ═══════════════════════════════════════════════════════════════
  describe("executeMigration with custom endpoint", () => {
    it("executes with custom endpoint", async () => {
      model.sourceEnvId = "env-1";
      model.targetEnvId = "env-2";
      model.selectedDashboardIds = ["1"];
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      vi.mocked(api.postApi).mockResolvedValueOnce({ task_id: "task-1" });

      await model.executeMigration("/custom/migrate");

      expect(api.postApi).toHaveBeenCalledWith(
        "/custom/migrate",
        expect.objectContaining({
          selected_ids: ["1"],
          source_env_id: "env-1",
          target_env_id: "env-2",
        }),
      );
    });
  });
});
// #endregion MigrationModelTests
