// #region DashboardHubModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dashboard-hub]
// @BRIEF L1 unit tests for DashboardHubModel — no DOM render, mocks API, git, toast, i18n.
// @RELATION BINDS_TO -> [DashboardHubModel]
// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_toggle_filter_resets_page, test_clear_filter_resets_page]
// @TEST_INVARIANT: selection-cleaned-on-load -> VERIFIED_BY: [test_selection_cleaned_after_load]
// @TEST_INVARIANT: clear-selected-ids -> VERIFIED_BY: [test_clear_selected_ids]
// @TEST_EDGE: missing_field -> incomplete dashboard entry in API response
// @TEST_EDGE: invalid_type -> API returns unexpected shape
// @TEST_EDGE: external_fail -> API throws on loadDashboards
import { describe, it, expect, vi, beforeEach } from "vitest";

// ── Mocks ────────────────────────────────────────────────────────
vi.mock("$app/navigation", () => ({ goto: vi.fn() }));
vi.mock("$lib/api.js", () => ({
  api: { getDashboards: vi.fn(), postApi: vi.fn(), requestApi: vi.fn(), getValidationStatusBatch: vi.fn() },
}));
vi.mock("../../../services/gitService.js", () => ({
  gitService: {
    getStatus: vi.fn(), getConfigs: vi.fn(), init: vi.fn(), getStatusesBatch: vi.fn(),
    sync: vi.fn(), commit: vi.fn(), pull: vi.fn(), push: vi.fn(), initRepository: vi.fn(),
  },
}));
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));
vi.mock("$lib/toasts.svelte.js", () => ({ addToast: vi.fn() }));
vi.mock("$lib/stores/taskDrawer.svelte.js", () => ({ openDrawerForTask: vi.fn(), openDrawerForTaskIfPreferred: vi.fn() }));
vi.mock("$lib/routes.js", () => ({ ROUTES: { dashboards: { list: vi.fn(), detail: vi.fn((r, e) => `/dashboards/${r}?env=${e}`) } } }));
vi.mock("$lib/i18n/index.svelte.js", () => ({
  t: { subscribe: (fn: (v: unknown) => void) => { fn({ common: { loading: "Loading" }, git: { no_servers_configured: "No Git", remote_url: "URL", init_success: "Init OK", sync_success: "Synced", commit_message: "Msg", commit_success: "Commit OK", nothing_to_commit: "No changes", not_linked: "Not linked", pull_success: "Pull OK", push_success: "Push OK" }, dashboard: { status_no_repo: "No Repo", status_changes: "Diff", status_no_changes: "Synced", load_failed: "Load failed", backup_task_failed: "Backup failed" } }); return () => {}; }, },
}));
vi.mock("../../../routes/dashboards/dashboard-helpers.js", () => ({
  normalizeTaskStatus: vi.fn((s) => s || ""), normalizeValidationStatus: vi.fn((s) => s || ""), normalizeOwners: vi.fn((o) => o || []), formatDate: vi.fn(() => "2024-06-10"),
}));

import { DashboardHubModel } from "../DashboardHubModel.svelte.ts";
import { api } from "$lib/api.js";
import { gitService } from "../../../services/gitService.js";
import { addToast } from "$lib/toasts.svelte.js";
import { openDrawerForTask, openDrawerForTaskIfPreferred } from "$lib/stores/taskDrawer.svelte.js";
import { goto } from "$app/navigation";

const makeDashResp = (overrides: Record<string, unknown> = {}) => ({
  dashboards: [
    { id: "dash-1", title: "Sales Dashboard", slug: "sales-dash", last_modified: "2024-06-10T10:00:00Z", owners: ["Alice"], git_status: { sync_status: "OK", branch: "main", has_repo: true, has_changes_for_commit: false }, last_task: { status: "success", validation_status: "PASS", task_id: "t-1" } },
    { id: "dash-2", title: "Marketing Dashboard", slug: "mkt-dash", last_modified: null, owners: [], git_status: null, last_task: null },
  ], total: 2, total_pages: 1, ...overrides,
});

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

  beforeEach(() => { vi.clearAllMocks(); model = new DashboardHubModel(); });

  describe("initial state", () => {
    it("starts with loading and empty lists", () => {
      expect(model.isLoading).toBe(true); expect(model.allDashboards).toEqual([]);
      expect(model.currentPage).toBe(1); expect(model.selectedIds.size).toBe(0);
      expect(model.selectedEnv).toBeNull(); expect(model.error).toBeNull();
    });
  });

  describe("setPage", () => {
    it("updates currentPage", () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp({ dashboards: [] }));
      model.setPage(3); expect(model.currentPage).toBe(3);
    });
    it("does nothing when page equals currentPage", () => { model.setPage(1); expect(api.getDashboards).not.toHaveBeenCalled(); });
  });

  describe("setPageSize", () => {
    it("updates pageSize and resets to page 1", () => {
      model.selectedEnv = "env-1"; model.currentPage = 5;
      vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp({ dashboards: [] }));
      model.setPageSize({ target: { value: "25" } } as any);
      expect(model.pageSize).toBe(25); expect(model.currentPage).toBe(1);
    });
  });

  describe("filter resets pagination", () => {
    it("toggleFilterValue resets page to 1", () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      model.currentPage = 5; model.toggleFilterValue("title", "Sales", true);
      expect(model.currentPage).toBe(1);
    });
    it("toggleFilterValue removes value when unchecked", () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      model.columnFilters.title = new Set(["Sales"]);
      model.toggleFilterValue("title", "Sales", false);
      expect(model.columnFilters.title.has("Sales")).toBe(false);
    });
    it("clearColumnFilter resets page to 1", () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      model.currentPage = 3; model.clearColumnFilter("title");
      expect(model.currentPage).toBe(1);
    });
    it("selectAllColumnFilterValues populates filter and resets page", () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      model.currentPage = 4; model.selectAllColumnFilterValues("title");
      expect(model.currentPage).toBe(1);
    });
  });

  describe("selection management", () => {
    it("clearSelectedIds empties selection", () => { model.selectedIds = new Set(["d1", "d2"]); model.clearSelectedIds(); expect(model.selectedIds.size).toBe(0); });
    it("replaceSelectedIds updates selection", () => { model.replaceSelectedIds(new Set(["d1", "d3"])); expect(model.selectedIds.has("d1")).toBe(true); expect(model.selectedIds.size).toBe(2); });
    it("handleCheckboxChange adds when checked", () => {
      model.handleCheckboxChange({ id: "d2" } as any, { target: { checked: true } } as any);
      expect(model.selectedIds.has("d2")).toBe(true);
    });
    it("handleCheckboxChange removes when unchecked", () => {
      model.selectedIds = new Set(["d1", "d2"]);
      model.handleCheckboxChange({ id: "d1" } as any, { target: { checked: false } } as any);
      expect(model.selectedIds.has("d1")).toBe(false);
    });
    it("handleSelectAll selects all filtered", () => {
      model.filteredDashboards = [{ id: "d1" }, { id: "d2" }] as any;
      model.handleSelectAll(); expect(model.selectedIds.has("d1")).toBe(true);
    });
    it("handleSelectAll clears when already all selected", () => {
      model.filteredDashboards = [{ id: "d1" }] as any; model.selectedIds = new Set(["d1"]);
      model.updateSelectionState(); expect(model.isAllSelected).toBe(true);
      model.handleSelectAll(); expect(model.selectedIds.size).toBe(0);
    });
    it("handleSelectVisible adds current page", () => {
      model.dashboards = [{ id: "p1" }] as any; model.handleSelectVisible();
      expect(model.selectedIds.has("p1")).toBe(true);
    });
    it("handleSelectVisible removes when already all visible selected", () => {
      model.dashboards = [{ id: "p1" }] as any; model.selectedIds = new Set(["p1"]);
      model.isAllVisibleSelected = true; model.handleSelectVisible();
      expect(model.selectedIds.has("p1")).toBe(false);
    });
    it("updateSelectionState computes isAllSelected", () => {
      model.filteredDashboards = [{ id: "d1" }, { id: "d2" }] as any; model.selectedIds = new Set(["d1", "d2"]);
      model.updateSelectionState(); expect(model.isAllSelected).toBe(true);
    });
  });

  describe("loadDashboards", () => {
    it("returns early when no selectedEnv", async () => { await model.loadDashboards(); expect(api.getDashboards).not.toHaveBeenCalled(); });
    it("loads and maps dashboards on success", async () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      await model.loadDashboards();
      expect(model.allDashboards).toHaveLength(2); expect(model.isLoading).toBe(false);
    });
    it("cleans orphan selectedIds after load", async () => {
      model.selectedEnv = "env-1"; model.selectedIds = new Set(["dash-1", "dash-orphan"]);
      vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      await model.loadDashboards();
      expect(model.selectedIds.has("dash-1")).toBe(true); expect(model.selectedIds.has("dash-orphan")).toBe(false);
    });
    it("handles API failure", async () => {
      model.selectedEnv = "env-1"; vi.mocked(api.getDashboards).mockRejectedValue(new Error("API Error"));
      await model.loadDashboards(); expect(model.error).toBeTruthy();
    });
    it("maps dashboard with null git_status and last_task", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      await model.loadDashboards();
      expect(model.allDashboards[1].git.hasRepo).toBeNull();
      expect(model.allDashboards[1].lastTask).toBeNull();
    });
    it("handles requestSeq race condition", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDashboards).mockImplementation(async () => {
        model.dashboardsLoadSeq = 2;
        return makeDashResp();
      });
      await model.loadDashboards();
      expect(model.isLoading).toBe(true);
    });
  });

  describe("loadDashboardSearchOptions", () => {
    it("populates options on success", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDashboards).mockResolvedValue({ dashboards: [{ id: "d1", title: "Sales", slug: "s", last_modified: "2024-06-10T10:00:00Z" }] });
      await model.loadDashboardSearchOptions();
      expect(model.searchableDashboardOptions).toHaveLength(1);
      expect(model.searchableDashboardLoading).toBe(false);
    });
    it("handles error", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDashboards).mockRejectedValue(new Error("Search error"));
      await model.loadDashboardSearchOptions();
      expect(model.searchableDashboardOptions).toEqual([]);
    });
    it("returns early without env", async () => {
      await model.loadDashboardSearchOptions();
      expect(api.getDashboards).not.toHaveBeenCalled();
    });
  });

  describe("fetchValidationStatuses", () => {
    it("fetches batch validation statuses", async () => {
      model.selectedEnv = "env-1"; model.dashboards = [{ id: "d1" }, { id: "d2" }] as any;
      vi.mocked(api.getValidationStatusBatch).mockResolvedValue({ d1: { status: "PASS" } });
      await model.fetchValidationStatuses();
      expect(api.getValidationStatusBatch).toHaveBeenCalled();
      expect(model.validationStatuses.d1).toBeDefined();
    });
    it("returns early without env", async () => { await model.fetchValidationStatuses(); expect(api.getValidationStatusBatch).not.toHaveBeenCalled(); });
    it("returns early when no dashboards", async () => { model.selectedEnv = "env-1"; await model.fetchValidationStatuses(); expect(api.getValidationStatusBatch).not.toHaveBeenCalled(); });
    it("handles error gracefully", async () => {
      model.selectedEnv = "env-1"; model.dashboards = [{ id: "d1" }] as any;
      vi.mocked(api.getValidationStatusBatch).mockRejectedValue(new Error("Status error"));
      await model.fetchValidationStatuses();
      expect(model.validationStatusLoading).toBe(false);
    });
  });

  describe("column filters", () => {
    it("hasColumnFilter returns true when filter has values", () => { model.columnFilters.title = new Set(["Sales"]); expect(model.hasColumnFilter("title")).toBe(true); });
    it("hasColumnFilter returns false when empty", () => { expect(model.hasColumnFilter("title")).toBe(false); });
    it("toggleFilterDropdown opens and closes", () => {
      model.toggleFilterDropdown("title", undefined);
      expect(model.openFilterColumn).toBe("title");
      model.toggleFilterDropdown("title", undefined);
      expect(model.openFilterColumn).toBeNull();
    });
    it("toggleFilterDropdown positions at trigger", () => {
      const e = { stopPropagation: vi.fn(), currentTarget: { getBoundingClientRect: () => ({ left: 200, bottom: 400 }) } } as any;
      model.toggleFilterDropdown("title", e);
      expect(model.filterDropdownPosition.left).toBe(200);
      expect(model.filterDropdownPosition.top).toBe(408);
    });
    it("getColumnCellValue returns all column values", () => {
      const dash = { id: "d1", title: "Sales", slug: "s", changedOn: "2024-06-10T10:00:00Z", actorLabel: "Alice", git: { status: "ok" } } as any;
      expect(model.getColumnCellValue(dash, "title")).toBe("Sales");
      expect(model.getColumnCellValue(dash, "git_status")).toBe("ok");
      expect(model.getColumnCellValue(dash, "changed_on")).toBe("2024-06-10");
      expect(model.getColumnCellValue(dash, "actor")).toBe("Alice");
      expect(model.getColumnCellValue(dash, "validation_status")).toBe("unknown");
    });
    it("getFilterOptions returns sorted unique values", () => {
      model.allDashboards = [
        { id: "d1", title: "Beta", git: { status: "ok" }, actorLabel: "Zoe", changedOn: null } as any,
        { id: "d2", title: "Alpha", git: { status: "ok" }, actorLabel: "Alice", changedOn: "2024-06-10" } as any,
      ];
      const titles = model.getFilterOptions("title");
      expect(titles).toEqual(["Alpha", "Beta"]);
    });
    it("getVisibleFilterOptions filters by search", () => {
      model.allDashboards = [{ id: "d1", title: "Sales" }, { id: "d2", title: "Marketing" }] as any;
      model.columnFilterSearch.title = "mark";
      const opts = model.getVisibleFilterOptions("title");
      expect(opts).toEqual(["Marketing"]);
    });
  });

  describe("sort", () => {
    it("handleSort toggles direction on same column", () => {
      model.sortColumn = "title"; model.sortDirection = "asc";
      model.handleSort("title"); expect(model.sortDirection).toBe("desc");
    });
    it("handleSort changes column and resets direction", () => {
      model.sortColumn = "title"; model.sortDirection = "desc";
      model.handleSort("changed_on");
      expect(model.sortColumn).toBe("changed_on"); expect(model.sortDirection).toBe("asc");
    });
    it("getSortValue returns values for all columns", () => {
      const dash = { title: "Sales", changedOn: "2024-06-10T10:00:00Z", actorLabel: "Alice", git: { status: "ok" } } as any;
      expect(model.getSortValue(dash, "title")).toBe("sales");
      expect(model.getSortValue(dash, "actor")).toBe("alice");
      expect(typeof model.getSortValue(dash, "changed_on")).toBe("number");
      expect(model.getSortValue(dash, "validation_status")).toBe("unknown");
    });
  });

  describe("profile filter", () => {
    it("handleTemporaryShowAll sets override and resets", () => {
      model.selectedEnv = "env-1"; model.currentPage = 3;
      vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      model.handleTemporaryShowAll();
      expect(model.profileFilterOverrideShowAll).toBe(true); expect(model.currentPage).toBe(1);
    });
    it("handleTemporaryShowAll does nothing if already set", () => {
      model.profileFilterOverrideShowAll = true; model.handleTemporaryShowAll();
      expect(api.getDashboards).not.toHaveBeenCalled();
    });
    it("handleRestoreProfileFilter clears override", () => {
      model.selectedEnv = "env-1"; model.profileFilterOverrideShowAll = true;
      vi.mocked(api.getDashboards).mockResolvedValue(makeDashResp());
      model.handleRestoreProfileFilter();
      expect(model.profileFilterOverrideShowAll).toBe(false);
    });
    it("handleRestoreProfileFilter does nothing if not set", () => {
      model.handleRestoreProfileFilter();
      expect(api.getDashboards).not.toHaveBeenCalled();
    });
  });

  describe("toggleActionDropdown", () => {
    it("opens and closes", () => {
      const e = { stopPropagation: vi.fn() } as any;
      model.toggleActionDropdown("d1", e);
      expect(model.openActionDropdown).toBe("d1");
      model.toggleActionDropdown("d1", e);
      expect(model.openActionDropdown).toBeNull();
    });
  });

  describe("git helpers", () => {
    it("getGitSummaryLabel returns Loading for null hasRepo", () => { expect(model.getGitSummaryLabel({ git: { hasRepo: null } } as any)).toBe("Loading"); });
    it("getGitSummaryLabel returns No Repo", () => { expect(model.getGitSummaryLabel({ git: { hasRepo: false } } as any)).toBe("No Repo"); });
    it("getGitSummaryLabel returns Diff for changes", () => { expect(model.getGitSummaryLabel({ git: { hasRepo: true, hasChangesForCommit: true } } as any)).toBe("Diff"); });
    it("getGitSummaryLabel returns Synced for clean", () => { expect(model.getGitSummaryLabel({ git: { hasRepo: true, hasChangesForCommit: false } } as any)).toBe("Synced"); });
    it("isGitBusy checks gitBusyIds", () => { model.gitBusyIds = new Set(["d1"]); expect(model.isGitBusy("d1")).toBe(true); });
    it("setGitBusy adds and removes", () => { model.setGitBusy("d1", true); expect(model.gitBusyIds.has("d1")).toBe(true); model.setGitBusy("d1", false); expect(model.gitBusyIds.has("d1")).toBe(false); });

    it("ensureGitConfigs returns cached", async () => {
      model.cachedGitConfigs = [{ id: 1, provider: "gitlab" }];
      expect(await model.ensureGitConfigs()).toEqual([{ id: 1, provider: "gitlab" }]);
    });
    it("ensureGitConfigs fetches when cache empty", async () => {
      vi.mocked(gitService.getConfigs).mockResolvedValue([{ id: 1 }]);
      expect(await model.ensureGitConfigs()).toEqual([{ id: 1 }]);
    });
    it("normalizeRepositoryStatusPayload handles explicit false", () => {
      expect(model.normalizeRepositoryStatusPayload({ has_repo: false }).hasRepo).toBe(false);
    });
    it("normalizeRepositoryStatusPayload handles dirty repo", () => {
      const r = model.normalizeRepositoryStatusPayload({ has_repo: true, is_dirty: true, current_branch: "dev" });
      expect(r.hasRepo).toBe(true); expect(r.hasChangesForCommit).toBe(true);
    });
    it("normalizeRepositoryStatusPayload handles untracked files", () => {
      const r = model.normalizeRepositoryStatusPayload({ has_repo: true, untracked_files: ["new.sql"] });
      expect(r.hasChangesForCommit).toBe(true);
    });
    it("normalizeRepositoryStatusPayload handles clean", () => {
      const r = model.normalizeRepositoryStatusPayload({ has_repo: true });
      expect(r.hasChangesForCommit).toBe(false);
    });
    it("normalizeRepositoryStatusPayload handles NO_REPO sync_state", () => {
      const r = model.normalizeRepositoryStatusPayload({ sync_state: "NO_REPO" });
      expect(r.hasRepo).toBe(false);
    });
    it("normalizeRepositoryStatusPayload handles changes via sync_state", () => {
      const r = model.normalizeRepositoryStatusPayload({ sync_state: "CHANGES", has_repo: true });
      expect(r.hasChangesForCommit).toBe(true);
    });
    it("normalizeRepositoryStatusPayload handles diff via sync_status", () => {
      const r = model.normalizeRepositoryStatusPayload({ sync_status: "DIFF", has_repo: true });
      expect(r.hasChangesForCommit).toBe(true);
    });
    it("updateDashboardGitState updates across all arrays", () => {
      const dash = { id: "d1", title: "T", git: { status: "ok" } } as any;
      model.allDashboards = [dash]; model.filteredDashboards = [dash]; model.dashboards = [dash];
      model.updateDashboardGitState("d1", { status: "diff" });
      expect(model.allDashboards[0].git.status).toBe("diff");
    });
  });

  describe("fetchDashboardGitStatusesBatch", () => {
    it("fetches and updates git states", async () => {
      vi.mocked(gitService.getStatusesBatch).mockResolvedValue({ statuses: { d1: { has_repo: true, is_dirty: false, current_branch: "main" } } });
      await model.fetchDashboardGitStatusesBatch(["d1"]);
      expect(model.gitLoadingIds.has("d1")).toBe(false);
      expect(model.gitResolvedIds.has("d1")).toBe(true);
    });
    it("updates to no_repo on batch error", async () => {
      const dash = { id: "d1", title: "T", git: { status: "ok" } } as any;
      model.allDashboards = [dash]; model.dashboards = [dash]; model.filteredDashboards = [dash];
      vi.mocked(gitService.getStatusesBatch).mockRejectedValue(new Error("Git error"));
      await model.fetchDashboardGitStatusesBatch(["d1"]);
      expect(model.allDashboards[0].git.hasRepo).toBe(false);
    });
    it("returns early with empty array", async () => {
      await model.fetchDashboardGitStatusesBatch([]);
      expect(gitService.getStatusesBatch).not.toHaveBeenCalled();
    });
    it("returns early when all resolved", async () => {
      model.gitResolvedIds.add("d1");
      model.gitResolvedIds = new Set(model.gitResolvedIds);
      await model.fetchDashboardGitStatusesBatch(["d1"]);
      expect(gitService.getStatusesBatch).not.toHaveBeenCalled();
    });
  });

  describe("hydrateVisibleGitStatusesBatch", () => {
    it("fetches pending git statuses", async () => {
      model.dashboards = [{ id: "d1", git: { hasRepo: null } }] as any;
      vi.mocked(gitService.getStatusesBatch).mockResolvedValue({ statuses: {} });
      await model.hydrateVisibleGitStatusesBatch();
      expect(gitService.getStatusesBatch).toHaveBeenCalled();
    });
    it("does nothing when no pending", async () => {
      await model.hydrateVisibleGitStatusesBatch();
      expect(gitService.getStatusesBatch).not.toHaveBeenCalled();
    });
  });

  describe("handleGitInit", () => {
    it("shows error when no git configs", async () => {
      vi.mocked(gitService.getConfigs).mockResolvedValue([]);
      await model.handleGitInit({ id: "d1" } as any);
      expect(addToast).toHaveBeenCalledWith("No Git config found", "error");
    });
    it("initializes repository successfully", async () => {
      const promptSpy = vi.spyOn(window, "prompt").mockReturnValue("https://git.example.com/repo.git");
      vi.mocked(gitService.getConfigs).mockResolvedValue([{ id: 1, url: "https://git.example.com", default_repository: "repo" }]);
      vi.mocked(gitService.initRepository).mockResolvedValue({});
      await model.handleGitInit({ id: "d1", slug: "dash-1" } as any);
      expect(gitService.initRepository).toHaveBeenCalled();
      expect(addToast).toHaveBeenCalledWith("Repository initialized", "success");
      promptSpy.mockRestore();
    });
    it("aborts when user cancels prompt", async () => {
      const promptSpy = vi.spyOn(window, "prompt").mockReturnValue(null);
      vi.mocked(gitService.getConfigs).mockResolvedValue([{ id: 1 }]);
      await model.handleGitInit({ id: "d1" } as any);
      expect(gitService.initRepository).not.toHaveBeenCalled();
      promptSpy.mockRestore();
    });
    it("handles init error", async () => {
      const promptSpy = vi.spyOn(window, "prompt").mockReturnValue("https://git.example.com/repo.git");
      vi.mocked(gitService.getConfigs).mockResolvedValue([{ id: 1 }]);
      vi.mocked(gitService.initRepository).mockRejectedValue(new Error("Init failed"));
      await model.handleGitInit({ id: "d1" } as any);
      expect(addToast).toHaveBeenCalledWith("Init failed", "error");
      promptSpy.mockRestore();
    });
  });

  describe("handleGit operations", () => {
    beforeEach(() => { model.selectedEnv = "env-1"; });

    it("handleGitSync succeeds", async () => {
      vi.mocked(gitService.sync).mockResolvedValue({});
      await model.handleGitSync({ id: "d1" } as any);
      expect(addToast).toHaveBeenCalledWith("Synced", "success");
    });
    it("handleGitSync handles error", async () => {
      vi.mocked(gitService.sync).mockRejectedValue(new Error("Sync failed"));
      await model.handleGitSync({ id: "d1" } as any);
      expect(addToast).toHaveBeenCalledWith("Sync failed", "error");
    });

    it("handleGitPull succeeds", async () => {
      vi.mocked(gitService.pull).mockResolvedValue({});
      await model.handleGitPull({ id: "d1", slug: "dash-1", git: { hasRepo: true } } as any);
      expect(gitService.pull).toHaveBeenCalled();
    });
    it("handleGitPull guards no repo", async () => {
      await model.handleGitPull({ id: "d1", git: { hasRepo: false } } as any);
      expect(gitService.pull).not.toHaveBeenCalled();
    });
    it("handleGitPull handles error", async () => {
      vi.mocked(gitService.pull).mockRejectedValue(new Error("Pull failed"));
      await model.handleGitPull({ id: "d1", git: { hasRepo: true } } as any);
      expect(addToast).toHaveBeenCalledWith("Pull failed", "error");
    });

    it("handleGitPush succeeds", async () => {
      vi.mocked(gitService.push).mockResolvedValue({});
      await model.handleGitPush({ id: "d1", slug: "dash-1", git: { hasRepo: true } } as any);
      expect(gitService.push).toHaveBeenCalled();
    });
    it("handleGitPush guards no repo", async () => {
      await model.handleGitPush({ id: "d1", git: { hasRepo: false } } as any);
      expect(gitService.push).not.toHaveBeenCalled();
    });
    it("handleGitPush handles error", async () => {
      vi.mocked(gitService.push).mockRejectedValue(new Error("Push failed"));
      await model.handleGitPush({ id: "d1", git: { hasRepo: true } } as any);
      expect(addToast).toHaveBeenCalledWith("Push failed", "error");
    });

    it("handleGitCommit succeeds", async () => {
      const promptSpy = vi.spyOn(window, "prompt").mockReturnValue("Fix bug");
      vi.mocked(gitService.commit).mockResolvedValue({});
      await model.handleGitCommit({ id: "d1", slug: "dash-1", title: "Dash", git: { hasRepo: true, hasChangesForCommit: true } } as any);
      expect(gitService.commit).toHaveBeenCalled();
      promptSpy.mockRestore();
    });
    it("handleGitCommit handles error", async () => {
      const promptSpy = vi.spyOn(window, "prompt").mockReturnValue("Fix bug");
      vi.mocked(gitService.commit).mockRejectedValue(new Error("Commit failed"));
      await model.handleGitCommit({ id: "d1", slug: "dash-1", git: { hasRepo: true, hasChangesForCommit: true } } as any);
      expect(addToast).toHaveBeenCalledWith("Commit failed", "error");
      promptSpy.mockRestore();
    });
    it("handleGitCommit blocks when no repo", async () => {
      await model.handleGitCommit({ id: "d1", git: { hasRepo: false }, title: "T" } as any);
      expect(gitService.commit).not.toHaveBeenCalled();
    });
    it("handleGitCommit blocks when no changes", async () => {
      await model.handleGitCommit({ id: "d1", git: { hasRepo: true, hasChangesForCommit: false }, title: "T" } as any);
      expect(gitService.commit).not.toHaveBeenCalled();
    });
  });

  describe("handleAction", () => {
    it("opens migrate modal", async () => {
      model.selectedEnv = "env-1";
      await model.handleAction({ id: "d1", title: "Sales" } as any, "migrate");
      expect(model.showMigrateModal).toBe(true);
    });
    it("opens backup modal", async () => {
      await model.handleAction({ id: "d1", title: "Sales" } as any, "backup");
      expect(model.showBackupModal).toBe(true);
    });
    it("closeActionDropdown clears state", () => { model.openActionDropdown = "d1"; model.closeActionDropdown(); expect(model.openActionDropdown).toBeNull(); });
  });

  describe("handleBulkBackup", () => {
    it("returns early when already submitting", async () => { model.isSubmittingBackup = true; await model.handleBulkBackup(); expect(api.postApi).not.toHaveBeenCalled(); });
    it("returns early when no selection", async () => { await model.handleBulkBackup(); expect(api.postApi).not.toHaveBeenCalled(); });
    it("posts backup and clears selection on success", async () => {
      model.selectedEnv = "env-1"; model.selectedIds = new Set(["d1", "d2"]);
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });
      await model.handleBulkBackup();
      expect(api.postApi).toHaveBeenCalledWith("/dashboards/backup", expect.any(Object));
      expect(model.selectedIds.size).toBe(0);
      expect(model.isSubmittingBackup).toBe(false);
    });
    it("handles error path", async () => {
      model.selectedEnv = "env-1"; model.selectedIds = new Set(["d1"]);
      vi.mocked(api.postApi).mockRejectedValue(new Error("Backup error"));
      const alertSpy = vi.spyOn(window, "alert").mockReturnValue();
      await model.handleBulkBackup();
      expect(alertSpy).toHaveBeenCalled();
      expect(model.isSubmittingBackup).toBe(false);
      alertSpy.mockRestore();
    });
  });

  describe("navigation", () => {
    it("navigateToDashboardDetail navigates with env", () => {
      model.selectedEnv = "env-1"; model.navigateToDashboardDetail({ id: "d1", slug: "sales" } as any);
      expect(goto).toHaveBeenCalled();
    });
    it("navigateToDashboardDetail does nothing without env", () => { model.navigateToDashboardDetail({ id: "d1" } as any); expect(goto).not.toHaveBeenCalled(); });
    it("handleTaskStatusClick opens drawer", () => { model.handleTaskStatusClick({ lastTask: { id: "t-1" } } as any); expect(openDrawerForTask).toHaveBeenCalledWith("t-1"); });
    it("handleTaskStatusClick does nothing without task id", () => { model.handleTaskStatusClick({ lastTask: null } as any); expect(openDrawerForTask).not.toHaveBeenCalled(); });
  });

  describe("validation popover", () => {
    it("toggleValidationPopover opens and closes", () => {
      const e = { stopPropagation: vi.fn(), currentTarget: { getBoundingClientRect: () => ({ left: 100, bottom: 200 }) } } as any;
      model.toggleValidationPopover("d1", e);
      expect(model.openValidationPopover).toBe("d1");
      model.toggleValidationPopover("d1", e);
      expect(model.openValidationPopover).toBeNull();
    });
    it("getValidationLabelForCell returns status", () => {
      model.validationStatuses = { d1: { status: "PASS" } };
      expect(model.getValidationLabelForCell({ id: "d1" } as any)).toBe("pass");
    });
    it("getValidationLabelForCell returns unknown for missing", () => {
      expect(model.getValidationLabelForCell({ id: "d2" } as any)).toBe("unknown");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // updateColumnFilterSearch
  // ═══════════════════════════════════════════════════════════════
  describe("updateColumnFilterSearch", () => {
    it("updates columnFilterSearch for given column", () => {
      model.updateColumnFilterSearch("title", "sales");
      expect(model.columnFilterSearch.title).toBe("sales");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // applyGridTransforms — filterDashboardIds branch
  // ═══════════════════════════════════════════════════════════════
  describe("applyGridTransforms", () => {
    it("filters by filterDashboardIds when set", () => {
      model.allDashboards = [
        { id: "d1", title: "Sales" } as any,
        { id: "d2", title: "Marketing" } as any,
        { id: "d3", title: "HR" } as any,
      ];
      model.filterDashboardIds = ["d1", "d2"];
      model.applyGridTransforms();
      expect(model.dashboards).toHaveLength(2);
    });

    it("sorts by title ascending by default", () => {
      model.allDashboards = [
        { id: "d2", title: "Beta" } as any,
        { id: "d1", title: "Alpha" } as any,
      ];
      model.applyGridTransforms();
      expect(model.dashboards[0].title).toBe("Alpha");
    });

    it("sorts descending when sortDirection is desc", () => {
      model.allDashboards = [
        { id: "d1", title: "Alpha" } as any,
        { id: "d2", title: "Beta" } as any,
      ];
      model.sortDirection = "desc";
      model.applyGridTransforms();
      expect(model.dashboards[0].title).toBe("Beta");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleBulkBackup — object task_id
  // ═══════════════════════════════════════════════════════════════
  describe("handleBulkBackup — object task_id", () => {
    it("handles task_id as object", async () => {
      model.selectedEnv = "env-1"; model.selectedIds = new Set(["d1"]);
      vi.mocked(api.postApi).mockResolvedValue({ task_id: { id: "obj-task-1" } });
      await model.handleBulkBackup();
      expect(model.isSubmittingBackup).toBe(false);
    });

    it("handles task_id as nested task_id object", async () => {
      model.selectedEnv = "env-1"; model.selectedIds = new Set(["d1"]);
      vi.mocked(api.postApi).mockResolvedValue({ task_id: { task_id: "nested-task-1" } });
      await model.handleBulkBackup();
      expect(model.isSubmittingBackup).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadDashboardSearchOptions with query param
  // ═══════════════════════════════════════════════════════════════
  describe("loadDashboardSearchOptions with query", () => {
    it("passes query to API", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDashboards).mockResolvedValue({ dashboards: [] });
      await model.loadDashboardSearchOptions("sales");
      expect(api.getDashboards).toHaveBeenCalledWith("env-1", expect.objectContaining({ search: "sales" }));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // refreshDashboardGitState
  // ═══════════════════════════════════════════════════════════════
  describe("refreshDashboardGitState", () => {
    it("calls fetchDashboardGitStatusesBatch with force", async () => {
      const batchSpy = vi.spyOn(model, "fetchDashboardGitStatusesBatch").mockResolvedValue();
      await model.refreshDashboardGitState("d1", true);
      expect(batchSpy).toHaveBeenCalledWith(["d1"], true);
      batchSpy.mockRestore();
    });
  });

  describe("coverage — remaining paths", () => {
    it("getColumnCellValue returns default dash", () => {
      expect(model.getColumnCellValue({} as any, "unknown" as any)).toBe("-");
    });

    it("getSortValue handles git_status and unknown column", () => {
      const dash = { git: { status: "ok" }, actorLabel: "Alice" } as any;
      expect(typeof model.getSortValue(dash, "git_status")).toBe("string");
      expect(model.getSortValue(dash, "unknown" as any)).toBe("");
    });

    it("handleGitCommit cancels on empty prompt", async () => {
      const promptSpy = vi.spyOn(window, "prompt").mockReturnValue("");
      await model.handleGitCommit({ id: "d1", title: "T", slug: "s", git: { hasRepo: true, hasChangesForCommit: true } } as any);
      expect(gitService.commit).not.toHaveBeenCalled();
      promptSpy.mockRestore();
    });

    it("fetchValidationStatuses returns early when IDs empty", async () => {
      model.selectedEnv = "env-1";
      model.dashboards = [{ id: "" }] as any;
      await model.fetchValidationStatuses();
      expect(api.getValidationStatusBatch).not.toHaveBeenCalled();
    });

    it("applyGridTransforms returns 0 for equal sort values", () => {
      model.allDashboards = [{ id: "d1", title: "Same" } as any, { id: "d2", title: "Same" } as any];
      model.applyGridTransforms();
      expect(model.dashboards).toHaveLength(2);
    });

    it("handleAction catch block on error", async () => {
      model.selectedEnv = "env-1";
      Object.defineProperty(model, "selectedIds", {
        get: () => { throw new Error("Access error"); }, configurable: true,
      });
      await model.handleAction({ id: "d1", title: "T" } as any, "migrate");
      // Catch block swallows error, no crash
      expect(model.showMigrateModal).toBe(false);
    });

    it("loadDashboards race condition in catch block", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDashboards).mockImplementation(async () => {
        model.dashboardsLoadSeq = 99;
        throw new Error("API Error");
      });
      await model.loadDashboards();
      // Error is NOT set because race condition causes early return in catch
      expect(model.error).toBeNull();
    });
  });
});

// #endregion DashboardHubModelTests
