// 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-clears-dryrun -> VERIFIED_BY: [test_selectSourceEnv_clears_dryRunResult]
// @TEST_INVARIANT: migration-blocked-same-env -> VERIFIED_BY: [test_executeMigration_blocked_same_env]
// @TEST_INVARIANT: dryrun-required-before-execute -> VERIFIED_BY: [test_canExecute_false_without_dryRunResult]
// @TEST_EDGE: missing_field -> missing env ids trigger precondition fails
// @TEST_EDGE: invalid_type -> API returns unexpected shape
// @TEST_EDGE: external_fail -> API throws on fetchDatabases
import { describe, it, expect, vi, beforeEach } from "vitest";

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";
import { resumeTask } from "../../../services/taskService.js";

describe("MigrationModel — L1 invariants (no render)", () => {
  let model: MigrationModel;
  beforeEach(() => { vi.clearAllMocks(); model = new MigrationModel(); selectedTask.current = null; });

  describe("initial state", () => {
    it("starts with defaults", () => {
      expect(model.currentStep).toBe(1); expect(model.sourceEnvId).toBe(""); expect(model.targetEnvId).toBe("");
      expect(model.selectedDashboardIds).toEqual([]); expect(model.dryRunResult).toBeNull(); expect(model.error).toBe("");
      expect(model.loading).toBe(true); expect(model.replaceDb).toBe(false); expect(model.fixCrossFilters).toBe(true);
      expect(model.showPasswordPrompt).toBe(false);
    });
  });

  describe("selectSourceEnv — reset invariants", () => {
    it("resets selectedDashboardIds and dashboards", () => {
      model.selectedDashboardIds = ["1", "2"]; model.dashboards = [{ id: "1" }];
      model.selectSourceEnv("env-2");
      expect(model.selectedDashboardIds).toEqual([]);
    });
    it("does NOT reset if same env is selected again", () => {
      model.sourceEnvId = "env-1"; model.selectedDashboardIds = ["1"];
      model.selectSourceEnv("env-1");
      expect(model.selectedDashboardIds).toEqual(["1"]);
    });
    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.mappings).toEqual([]);
    });
    it("clears dryRunResult", () => {
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      model.selectSourceEnv("env-2");
      expect(model.dryRunResult).toBeNull();
    });
  });

  describe("selectTargetEnv", () => {
    it("sets targetEnvId and clears dryRunResult", () => {
      model.dryRunResult = { risk: { score: 10, level: "low", items: [] } };
      model.selectTargetEnv("env-2");
      expect(model.targetEnvId).toBe("env-2"); expect(model.dryRunResult).toBeNull();
    });
    it("does nothing when same env", () => { model.selectTargetEnv("env-2"); model.selectTargetEnv("env-2"); expect(model.targetEnvId).toBe("env-2"); });
  });

  describe("toggleDashboard", () => {
    it("adds id when not present", () => { model.toggleDashboard("5"); expect(model.selectedDashboardIds).toContain("5"); });
    it("removes id when present", () => { model.selectedDashboardIds = ["1", "5"]; model.toggleDashboard("5"); expect(model.selectedDashboardIds).toEqual(["1"]); });
    it("clears dryRunResult", () => { model.dryRunResult = {} as any; model.toggleDashboard("1"); expect(model.dryRunResult).toBeNull(); });
  });

  describe("selectAllDashboards / deselectAllDashboards", () => {
    it("selects all", () => { model.dashboards = [{ id: "1" }, { id: "2" }]; model.selectAllDashboards(); expect(model.selectedDashboardIds).toEqual(["1", "2"]); });
    it("deselects all", () => { model.selectedDashboardIds = ["1"]; model.deselectAllDashboards(); expect(model.selectedDashboardIds).toEqual([]); });
  });

  describe("executeMigration blocked invariants", () => {
    it("blocks when source=target", 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 empty", async () => {
      model.sourceEnvId = ""; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"];
      await model.executeMigration();
      expect(model.error).toContain("both"); expect(api.postApi).not.toHaveBeenCalled();
    });
  });

  describe("canExecute derived", () => {
    it("false when dryRunResult is null", () => { model.selectedDashboardIds = ["1"]; expect(model.canExecute).toBe(false); });
    it("false when no dashboards", () => { model.dryRunResult = {} as any; model.selectedDashboardIds = []; expect(model.canExecute).toBe(false); });
    it("true when ready", () => { model.selectedDashboardIds = ["1"]; model.dryRunResult = { risk: { score: 10, level: "low", items: [] } }; expect(model.canExecute).toBe(true); });
  });

  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 same env", () => { model.sourceEnvId = "env-1"; model.targetEnvId = "env-1"; expect(model.stepReady[1]).toBe(false); });
    it("step 2 ready when dashboards selected", () => { model.selectedDashboardIds = ["1"]; expect(model.stepReady[2]).toBe(true); });
    it("step 3 always ready", () => { expect(model.stepReady[3]).toBe(true); });
  });

  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 = {} as any; model.setReplaceDb(true); expect(model.dryRunResult).toBeNull(); });
  });

  describe("setFixCrossFilters", () => {
    it("sets value and clears dryRunResult", () => { model.setFixCrossFilters(false); expect(model.fixCrossFilters).toBe(false); });
  });

  describe("goToStep gating", () => {
    it("allows backward navigation", () => { model.currentStep = 3; model.goToStep(1); expect(model.currentStep).toBe(1); });
    it("allows forward to step 2 when ready", () => { model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.goToStep(2); expect(model.currentStep).toBe(2); });
    it("blocks forward to step 2 when not ready", () => { model.goToStep(2); expect(model.currentStep).toBe(1); });
    it("allows step 4 only with dryRunResult", () => {
      model.currentStep = 3; model.dryRunResult = {} as any; model.selectedDashboardIds = ["1"]; model.sourceEnvId = "env-1"; model.targetEnvId = "env-2";
      model.goToStep(4); expect(model.currentStep).toBe(4);
    });
    it("blocks step 4 without dryRunResult", () => { model.currentStep = 3; model.goToStep(4); expect(model.currentStep).toBe(3); });
  });

  describe("checkPasswordPrompt", () => {
    it("shows prompt for AWAITING_INPUT with database_password", () => {
      selectedTask.current = { id: "t-1", status: "AWAITING_INPUT", input_request: { type: "database_password", databases: ["db1"], error_message: "Auth failed" } };
      model.checkPasswordPrompt();
      expect(model.showPasswordPrompt).toBe(true); expect(model.passwordPromptDatabases).toEqual(["db1"]);
    });
    it("does NOT show prompt when not AWAITING_INPUT", () => {
      selectedTask.current = { id: "t-1", status: "RUNNING" };
      model.checkPasswordPrompt(); expect(model.showPasswordPrompt).toBe(false);
    });
    it("does NOT show prompt for different input_request type", () => {
      selectedTask.current = { id: "t-1", status: "AWAITING_INPUT", input_request: { type: "confirmation" } };
      model.checkPasswordPrompt(); expect(model.showPasswordPrompt).toBe(false);
    });
    it("does nothing when no active task", () => { selectedTask.current = null; model.checkPasswordPrompt(); expect(model.showPasswordPrompt).toBe(false); });
  });

  describe("clearError", () => { it("clears error", () => { model.error = "Something"; model.clearError(); expect(model.error).toBe(""); }); });

  describe("screenState derived", () => {
    it("idle when loading", () => { model.loading = true; expect(model.screenState).toBe("idle"); });
    it("error when error set", () => { model.loading = false; model.error = "Fail"; expect(model.screenState).toBe("error"); });
    it("loading when dryRunLoading", () => { model.loading = false; model.dryRunLoading = true; expect(model.screenState).toBe("loading"); });
    it("review when dryRunResult exists", () => { model.loading = false; model.dryRunResult = {} as any; expect(model.screenState).toBe("review"); });
    it("ready when no special state", () => { model.loading = false; expect(model.screenState).toBe("ready"); });
    it("executing when currentStep >= 4", () => { model.loading = false; model.currentStep = 4; expect(model.screenState).toBe("executing"); });
  });

  describe("loadEnvironments", () => {
    it("sets loading lifecycle", async () => {
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce([]);
      const p = model.loadEnvironments(); expect(model.loading).toBe(true); await p; expect(model.loading).toBe(false);
    });
    it("pre-fills sourceEnvId from context", async () => {
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce([{ id: "env-1", name: "Dev" }]);
      await model.loadEnvironments(); expect(model.sourceEnvId).toBe("env-1");
    });
    it("sets error on failure", async () => {
      vi.mocked(api.getEnvironmentsList).mockRejectedValueOnce(new Error("Fail"));
      await model.loadEnvironments(); expect(model.error).toBe("Fail");
    });
  });

  describe("fetchDatabases", () => {
    it("returns early without env ids", async () => { await model.fetchDatabases(); expect(api.requestApi).not.toHaveBeenCalled(); });
    it("fetches in parallel", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2";
      vi.mocked(api.requestApi).mockResolvedValueOnce([{ uuid: "db-1", database_name: "DevDB" }]).mockResolvedValueOnce([{ uuid: "db-2", database_name: "ProdDB" }]).mockResolvedValueOnce([{ id: 1 }]);
      vi.mocked(api.postApi).mockResolvedValueOnce([]);
      await model.fetchDatabases();
      expect(model.sourceDatabases).toHaveLength(1); expect(model.fetchingDbs).toBe(false);
    });
    it("sets error on failure", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2";
      vi.mocked(api.requestApi).mockRejectedValue(new Error("DB fail"));
      await model.fetchDatabases(); expect(model.error).toBe("DB fail");
    });
  });

  describe("saveMapping", () => {
    it("does nothing when dbs not found", async () => { await model.saveMapping("missing", "tgt"); expect(api.postApi).not.toHaveBeenCalled(); });
    it("saves and updates mappings", async () => {
      model.sourceDatabases = [{ uuid: "suuid", database_name: "SrcDB" }]; model.targetDatabases = [{ uuid: "tuuid", database_name: "TgtDB" }];
      vi.mocked(api.postApi).mockResolvedValue({ id: 1 });
      await model.saveMapping("suuid", "tuuid");
      expect(model.mappings).toHaveLength(1);
    });
    it("sets error on failure", async () => {
      model.sourceDatabases = [{ uuid: "suuid", database_name: "S" }]; model.targetDatabases = [{ uuid: "tuuid", database_name: "T" }];
      vi.mocked(api.postApi).mockRejectedValue(new Error("Save failed"));
      await model.saveMapping("suuid", "tuuid"); expect(model.error).toBe("Save failed");
    });
  });

  describe("_validatePreconditions", () => {
    it("false when both envs empty", () => { expect(model._validatePreconditions()).toBe(false); expect(model.error).toContain("both"); });
    it("false when same env", () => { model.sourceEnvId = "env-1"; model.targetEnvId = "env-1"; expect(model._validatePreconditions()).toBe(false); });
    it("false when no dashboards", () => { model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; expect(model._validatePreconditions()).toBe(false); });
    it("true when all met", () => { model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"]; expect(model._validatePreconditions()).toBe(true); });
  });

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

  describe("executeMigration", () => {
    it("executes with default endpoint", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"]; model.dryRunResult = { risk: { score: 10, level: "low", items: [] } } as any;
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" }); vi.mocked(api.getTask).mockResolvedValue({ id: "task-1", status: "RUNNING" });
      await model.executeMigration();
      expect(api.postApi).toHaveBeenCalledWith("/migration/execute", expect.any(Object));
    });
    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: [] } } as any;
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });
      await model.executeMigration("/custom/migrate");
      expect(api.postApi).toHaveBeenCalledWith("/custom/migrate", expect.any(Object));
    });
    it("sets error on failure", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"]; model.dryRunResult = {} as any;
      vi.mocked(api.postApi).mockRejectedValue(new Error("Failed"));
      await model.executeMigration(); expect(model.error).toBe("Failed");
    });
  });

  describe("calculateDryRun → toggleDashboard invariant", () => {
    it("clears dryRunResult on toggle after dry-run", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"];
      vi.mocked(api.postApi).mockResolvedValueOnce({ summary: {}, risk: { score: 10, level: "low", items: [] }, selected_dashboard_titles: ["Sales"] });
      await model.calculateDryRun();
      expect(model.dryRunResult).toBeTruthy();
      model.toggleDashboard("1");
      expect(model.dryRunResult).toBeNull();
    });
  });

  describe("resumeMigration", () => {
    it("does nothing with no active task", async () => { selectedTask.current = null; await model.resumeMigration({}); expect(resumeTask).not.toHaveBeenCalled(); });
    it("resumes and closes prompt", async () => {
      selectedTask.current = { id: "task-1" };
      await model.resumeMigration({ pw: "secret" });
      expect(resumeTask).toHaveBeenCalledWith("task-1", { pw: "secret" }); expect(model.showPasswordPrompt).toBe(false);
    });
    it("sets error on failure", async () => {
      selectedTask.current = { id: "task-1" };
      vi.mocked(resumeTask).mockRejectedValue(new Error("Resume failed"));
      await model.resumeMigration({}); expect(model.passwordPromptErrorMessage).toBe("Resume failed");
    });
  });

  describe("log viewer / task history", () => {
    it("openLogViewer sets state", () => { model.openLogViewer({ id: "t-1", status: "RUNNING" }); expect(model.showLogViewer).toBe(true); expect(model.logViewerTaskId).toBe("t-1"); });
    it("closeLogViewer resets", () => { model.showLogViewer = true; model.closeLogViewer(); expect(model.showLogViewer).toBe(false); expect(model.logViewerTaskId).toBeNull(); });
    it("toggleTaskHistory toggles", () => { expect(model.showTaskHistory).toBe(false); model.toggleTaskHistory(); expect(model.showTaskHistory).toBe(true); model.toggleTaskHistory(); expect(model.showTaskHistory).toBe(false); });
  });

  describe("calculateDryRun", () => {
    it("returns early with invalid preconditions", async () => {
      await model.calculateDryRun();
      expect(api.postApi).not.toHaveBeenCalled();
      expect(model.dryRunLoading).toBe(false);
    });
    it("sets error on API failure", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"];
      vi.mocked(api.postApi).mockRejectedValue(new Error("Dry-run failed"));
      await model.calculateDryRun();
      expect(model.error).toBe("Dry-run failed");
      expect(model.dryRunResult).toBeNull();
      expect(model.dryRunLoading).toBe(false);
    });
    it("advances to step 3 on success", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"];
      vi.mocked(api.postApi).mockResolvedValue({ summary: {}, risk: { score: 10, level: "low", items: [] }, selected_dashboard_titles: ["Sales"] });
      await model.calculateDryRun();
      expect(model.currentStep).toBe(3);
      expect(model.dryRunResult).toBeTruthy();
    });
  });

  describe("executeMigration — fallback task", () => {
    it("creates fallback task when getTask fails", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2"; model.selectedDashboardIds = ["1"]; model.dryRunResult = {} as any;
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });
      vi.mocked(api.getTask).mockRejectedValue(new Error("Task not found"));
      await model.executeMigration();
      expect(api.postApi).toHaveBeenCalled();
    });
  });

  describe("_fetchDashboards", () => {
    it("populates dashboards on success", async () => {
      vi.mocked(api.requestApi).mockResolvedValue([{ id: "d1" }, { id: "d2" }]);
      await (model as any)._fetchDashboards("env-1");
      expect(model.dashboards).toHaveLength(2);
    });
    it("sets error on failure", async () => {
      vi.mocked(api.requestApi).mockRejectedValue(new Error("Fetch failed"));
      await (model as any)._fetchDashboards("env-1");
      expect(model.error).toBe("Fetch failed");
      expect(model.dashboards).toEqual([]);
    });
  });

  describe("fetchDatabases — lifecycle", () => {
    it("sets fetchingDbs true then false", async () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2";
      vi.mocked(api.requestApi).mockResolvedValue([]);
      vi.mocked(api.postApi).mockResolvedValue([]);
      const promise = model.fetchDatabases();
      expect(model.fetchingDbs).toBe(true);
      await promise;
      expect(model.fetchingDbs).toBe(false);
    });
  });

  describe("envsReady derived", () => {
    it("true when source and target envs distinct", () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-2";
      expect(model.envsReady).toBe(true);
    });
    it("false when same env", () => {
      model.sourceEnvId = "env-1"; model.targetEnvId = "env-1";
      expect(model.envsReady).toBe(false);
    });
  });
});
// #endregion MigrationModelTests
