// #region DashboardDetailModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dashboard-detail]
// @BRIEF L1 unit tests for DashboardDetailModel — no DOM render, mocks API + toast + i18n + navigation.
// @RELATION BINDS_TO -> [DashboardDetailModel]
// @TEST_INVARIANT: load-gate -> VERIFIED_BY: [test_load_guarded_by_context, test_load_detail_guarded]
// @TEST_INVARIANT: thumbnail-cleanup -> VERIFIED_BY: [test_cleanup_releases_thumbnail, test_load_thumbnail_releases_previous]
// @TEST_EDGE: missing_field -> dashboardRef not set on load
// @TEST_EDGE: invalid_type -> API returns non-object
// @TEST_EDGE: external_fail -> API throws on each fetch
import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("$app/navigation", () => ({ goto: vi.fn() }));
vi.mock("$lib/api.js", () => ({
  api: {
    getDashboardDetail: vi.fn(),
    getDashboardTaskHistory: vi.fn(),
    getDashboardThumbnail: vi.fn(),
    postApi: vi.fn(),
  },
  fetchApi: 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", () => ({
  openDrawerForTaskIfPreferred: vi.fn(),
  openDrawerForTask: vi.fn(),
}));
vi.mock("$lib/routes.js", () => ({
  ROUTES: {
    dashboards: { list: vi.fn(() => "/dashboards"), detail: vi.fn(() => "/dashboards/1") },
    datasets: { detail: vi.fn(() => "/datasets/1") },
  },
}));
vi.mock("$lib/i18n/index.svelte.js", () => ({
  getT: () => ({
    dashboard: {
      missing_context: "Missing context",
      load_detail_failed: "Failed",
      thumbnail_generating: "Generating...",
      thumbnail_failed: "Thumbnail failed",
      backup_started: "Backup started",
      backup_task_failed: "Backup failed",
      backup: "Backup",
      llm_check: "LLM Check",
    },
  }),
}));

// Mock URL.createObjectURL / revokeObjectURL
const mockObjectUrl = "blob:mock-url-1";
URL.createObjectURL = vi.fn(() => mockObjectUrl);
URL.revokeObjectURL = vi.fn();

import { DashboardDetailModel } from "../DashboardDetailModel.svelte.ts";
import { api } from "$lib/api.js";
import { fetchApi } from "$lib/api";
import { goto } from "$app/navigation";
import { addToast } from "$lib/toasts.svelte.js";
import { openDrawerForTaskIfPreferred } from "$lib/stores/taskDrawer.svelte.js";

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

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

  // ═══════════════════════════════════════════════════════════════
  // Initial state
  // ═══════════════════════════════════════════════════════════════
  describe("initial state", () => {
    it("starts with default values", () => {
      expect(model.screenState).toBe("idle");
      expect(model.dashboard).toBeNull();
      expect(model.error).toBeNull();
      expect(model.dashboardRef).toBe("");
      expect(model.envId).toBe("");
      expect(model.activeTab).toBe("resources");
      expect(model.taskHistory).toEqual([]);
      expect(model.isTaskHistoryLoading).toBe(false);
      expect(model.validationHistory).toEqual([]);
      expect(model.thumbnailUrl).toBe("");
      expect(model.isStartingBackup).toBe(false);
      expect(model.showGitManager).toBe(false);
    });

    it("derived isLoading equals screenState === 'loading'", () => {
      expect(model.isLoading).toBe(false);
      model.screenState = "loading";
      expect(model.isLoading).toBe(true);
    });

    it("derived gitDashboardRef falls through slug -> dashboardRef -> empty", () => {
      expect(model.gitDashboardRef).toBe("");
      model.dashboardRef = "my-ref";
      expect(model.gitDashboardRef).toBe("my-ref");
      model.dashboard = { id: 1, title: "Sales", slug: "sales-dash" };
      expect(model.gitDashboardRef).toBe("sales-dash");
    });

    it("derived resolvedDashboardId from numeric dashboardRef", () => {
      model.dashboardRef = "42";
      expect(model.resolvedDashboardId).toBe(42);
    });

    it("derived resolvedDashboardId from dashboard.id", () => {
      model.dashboard = { id: 99, title: "Sales" };
      expect(model.resolvedDashboardId).toBe(99);
    });

    it("derived resolvedDashboardId is null when ref is non-numeric and no dashboard", () => {
      model.dashboardRef = "slug-ref";
      expect(model.resolvedDashboardId).toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loadDashboardDetail guarded by context
  // ═══════════════════════════════════════════════════════════════
  describe("loadDashboardDetail", () => {
    it("sets error when dashboardRef is empty", async () => {
      model.envId = "env-1";
      await model.loadDashboardDetail();
      expect(model.screenState).toBe("error");
      expect(model.error).toBeTruthy();
    });

    it("sets error when envId is empty", async () => {
      model.dashboardRef = "dash-1";
      await model.loadDashboardDetail();
      expect(model.screenState).toBe("error");
      expect(model.error).toBeTruthy();
    });

    it("loads detail when dashboardRef and envId are set", async () => {
      vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 1, title: "Sales" });
      model.dashboardRef = "sales-dash";
      model.envId = "env-1";

      await model.loadDashboardDetail();

      expect(model.screenState).toBe("loaded");
      expect(model.dashboard?.title).toBe("Sales");
      expect(model.error).toBeNull();
    });

    it("sets loading state before fetch, then loaded after", async () => {
      vi.mocked(api.getDashboardDetail).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));
      model.dashboardRef = "sales-dash";
      model.envId = "env-1";

      const promise = model.loadDashboardDetail();
      expect(model.screenState).toBe("loading");
      await promise;
      expect(model.screenState).toBe("loaded");
    });

    it("sets error state on API failure", async () => {
      vi.mocked(api.getDashboardDetail).mockRejectedValue(new Error("404 Not Found"));
      model.dashboardRef = "sales-dash";
      model.envId = "env-1";

      await model.loadDashboardDetail();

      expect(model.screenState).toBe("error");
      expect(model.error).toBe("404 Not Found");
    });

    it("handles non-Error rejection gracefully", async () => {
      vi.mocked(api.getDashboardDetail).mockRejectedValue("string error");
      model.dashboardRef = "sales-dash";
      model.envId = "env-1";

      await model.loadDashboardDetail();

      expect(model.screenState).toBe("error");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadDashboardPage — parallel fetch
  // ═══════════════════════════════════════════════════════════════
  describe("loadDashboardPage", () => {
    it("loads detail, task history, thumbnail, validation in parallel", async () => {
      vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 1, title: "Sales", slug: "sales-dash" });
      vi.mocked(api.getDashboardTaskHistory).mockResolvedValue({ items: [] });
      vi.mocked(api.getDashboardThumbnail).mockRejectedValue({ status: 202 });
      vi.mocked(fetchApi).mockResolvedValue({ items: [] });

      model.dashboardRef = "sales-dash";
      model.envId = "env-1";

      await model.loadDashboardPage();

      expect(api.getDashboardDetail).toHaveBeenCalled();
      expect(api.getDashboardTaskHistory).toHaveBeenCalled();
      expect(api.getDashboardThumbnail).toHaveBeenCalled();
      expect(fetchApi).toHaveBeenCalledWith(expect.stringContaining("/validation-tasks/runs/all"));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadTaskHistory
  // ═══════════════════════════════════════════════════════════════
  describe("loadTaskHistory", () => {
    it("returns early when ref is empty", async () => {
      await model.loadTaskHistory();
      expect(api.getDashboardTaskHistory).not.toHaveBeenCalled();
    });

    it("loads task history on success", async () => {
      vi.mocked(api.getDashboardTaskHistory).mockResolvedValue({
        items: [{ id: "task-1", status: "success" }],
      });
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadTaskHistory();

      expect(api.getDashboardTaskHistory).toHaveBeenCalledWith("env-1", "dash-1", { limit: 30 });
      expect(model.taskHistory).toHaveLength(1);
      expect(model.isTaskHistoryLoading).toBe(false);
    });

    it("sets error on API failure", async () => {
      vi.mocked(api.getDashboardTaskHistory).mockRejectedValue(new Error("History failed"));
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadTaskHistory();

      expect(model.taskHistoryError).toBe("History failed");
      expect(model.taskHistory).toEqual([]);
      expect(model.isTaskHistoryLoading).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadThumbnail
  // ═══════════════════════════════════════════════════════════════
  describe("loadThumbnail", () => {
    it("returns early when ref is empty", async () => {
      await model.loadThumbnail();
      expect(api.getDashboardThumbnail).not.toHaveBeenCalled();
    });

    it("loads thumbnail and creates blob URL", async () => {
      const fakeBlob = new Blob(["fake"], { type: "image/png" });
      vi.mocked(api.getDashboardThumbnail).mockResolvedValue(fakeBlob);
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadThumbnail();

      expect(api.getDashboardThumbnail).toHaveBeenCalledWith("env-1", "dash-1", { force: false });
      expect(URL.createObjectURL).toHaveBeenCalledWith(fakeBlob);
      expect(model.thumbnailUrl).toBe(mockObjectUrl);
      expect(model.isThumbnailLoading).toBe(false);
    });

    it("handles 202 status as generating", async () => {
      vi.mocked(api.getDashboardThumbnail).mockRejectedValue({ status: 202 });
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadThumbnail();

      expect(model.thumbnailError).toBe("Generating...");
      expect(model.isThumbnailLoading).toBe(false);
    });

    it("handles other errors", async () => {
      vi.mocked(api.getDashboardThumbnail).mockRejectedValue(new Error("Network"));
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadThumbnail();

      expect(model.thumbnailError).toBe("Network");
      expect(model.isThumbnailLoading).toBe(false);
    });

    // @INVARIANT: thumbnail blob URL released on reassignment
    it("releases previous thumbnail URL when loading new one", async () => {
      const fakeBlob = new Blob(["fake"], { type: "image/png" });
      model.thumbnailUrl = "blob:old-url";
      vi.mocked(api.getDashboardThumbnail).mockResolvedValue(fakeBlob);
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadThumbnail();

      expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:old-url");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadValidationHistory
  // ═══════════════════════════════════════════════════════════════
  describe("loadValidationHistory", () => {
    it("returns early when ref is empty", async () => {
      await model.loadValidationHistory();
      expect(fetchApi).not.toHaveBeenCalled();
    });

    it("loads validation history on success via items", async () => {
      vi.mocked(fetchApi).mockResolvedValue({ items: [{ id: "vr-1", status: "completed" }] });
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadValidationHistory();

      expect(model.validationHistory).toHaveLength(1);
      expect(model.isValidationHistoryLoading).toBe(false);
    });

    it("falls back to results field when items is absent", async () => {
      vi.mocked(fetchApi).mockResolvedValue({ results: [{ id: "vr-2" }] });
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadValidationHistory();

      expect(model.validationHistory).toHaveLength(1);
    });

    it("sets error on API failure", async () => {
      vi.mocked(fetchApi).mockRejectedValue(new Error("Val failed"));
      model.dashboardRef = "dash-1";
      model.envId = "env-1";

      await model.loadValidationHistory();

      expect(model.validationHistoryError).toBe("Val failed");
      expect(model.validationHistory).toEqual([]);
      expect(model.isValidationHistoryLoading).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // runBackupTask
  // ═══════════════════════════════════════════════════════════════
  describe("runBackupTask", () => {
    it("returns early when isStartingBackup is true", async () => {
      model.isStartingBackup = true;
      await model.runBackupTask();
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("returns early when envId is empty", async () => {
      await model.runBackupTask();
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("returns early when resolvedDashboardId is null", async () => {
      model.envId = "env-1";
      model.dashboardRef = "slug-only";
      await model.runBackupTask();
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("posts backup and shows success toast", async () => {
      model.envId = "env-1";
      model.dashboard = { id: 1, title: "Sales" };
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });
      vi.mocked(api.getDashboardTaskHistory).mockResolvedValue({ items: [] });

      await model.runBackupTask();

      expect(api.postApi).toHaveBeenCalledWith("/dashboards/backup", {
        env_id: "env-1",
        dashboard_ids: [1],
      });
      expect(openDrawerForTaskIfPreferred).toHaveBeenCalledWith("task-1");
      expect(addToast).toHaveBeenCalledWith("Backup started", "success");
      expect(model.isStartingBackup).toBe(false);
    });

    it("shows error toast on API failure", async () => {
      model.envId = "env-1";
      model.dashboard = { id: 1, title: "Sales" };
      vi.mocked(api.postApi).mockRejectedValue(new Error("Backup failed"));

      await model.runBackupTask();

      expect(addToast).toHaveBeenCalledWith("Backup failed", "error");
      expect(model.isStartingBackup).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Git-related actions
  // ═══════════════════════════════════════════════════════════════
  describe("handleSyncAndOpenCommit", () => {
    it("opens git manager on sync success", async () => {
      model.gitModel.syncRepository = vi.fn().mockResolvedValue(true);
      await model.handleSyncAndOpenCommit();
      expect(model.showGitManager).toBe(true);
    });

    it("does not open git manager on sync failure", async () => {
      model.gitModel.syncRepository = vi.fn().mockResolvedValue(false);
      await model.handleSyncAndOpenCommit();
      expect(model.showGitManager).toBe(false);
    });
  });

  describe("trackGitManagerClose", () => {
    it("sets wasOpen when showGitManager becomes true", () => {
      model.showGitManager = true;
      model.trackGitManagerClose();
      // no assertion needed — internal flag set
    });

    it("reloads status when git manager closes after having been open", () => {
      model.showGitManager = true;
      model.trackGitManagerClose(); // marks wasOpen
      model.showGitManager = false;
      model.gitModel.loadStatus = vi.fn().mockResolvedValue(undefined);
      model.trackGitManagerClose(); // triggers reload
      expect(model.gitModel.loadStatus).toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // cleanup
  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: cleanup releases thumbnail
  it("cleanup releases thumbnail URL", () => {
    model.thumbnailUrl = "blob:some-url";
    model.cleanup();
    expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:some-url");
    expect(model.thumbnailUrl).toBe("");
  });

  // ═══════════════════════════════════════════════════════════════
  // Navigation
  // ═══════════════════════════════════════════════════════════════
  describe("goBack", () => {
    it("navigates to dashboard list", () => {
      model.envId = "env-1";
      model.goBack();
      expect(goto).toHaveBeenCalled();
    });
  });

  describe("openDataset", () => {
    it("navigates to dataset detail", () => {
      model.envId = "env-1";
      model.openDataset("ds-42");
      expect(goto).toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Static utilities
  // ═══════════════════════════════════════════════════════════════
  describe("static methods", () => {
    describe("toTaskTypeLabel", () => {
      it("returns backup label for superset-backup", () => {
        expect(DashboardDetailModel.toTaskTypeLabel("superset-backup")).toBe("Backup");
      });

      it("returns LLM check label for llm_dashboard_validation", () => {
        expect(DashboardDetailModel.toTaskTypeLabel("llm_dashboard_validation")).toBe("LLM Check");
      });

      it("returns pluginId as-is for unknown plugins", () => {
        expect(DashboardDetailModel.toTaskTypeLabel("custom-plugin")).toBe("custom-plugin");
      });

      it("returns dash for undefined", () => {
        expect(DashboardDetailModel.toTaskTypeLabel(undefined)).toBe("-");
      });
    });

    describe("getTaskStatusClasses", () => {
      it("returns primary class for running/pending", () => {
        expect(DashboardDetailModel.getTaskStatusClasses("running")).toContain("bg-primary-light");
        expect(DashboardDetailModel.getTaskStatusClasses("pending")).toContain("bg-primary-light");
        expect(DashboardDetailModel.getTaskStatusClasses("PENDING")).toContain("bg-primary-light");
      });

      it("returns success class for success", () => {
        expect(DashboardDetailModel.getTaskStatusClasses("success")).toContain("bg-success-light");
      });

      it("returns destructive class for failed/error", () => {
        expect(DashboardDetailModel.getTaskStatusClasses("failed")).toContain("bg-destructive-light");
        expect(DashboardDetailModel.getTaskStatusClasses("error")).toContain("bg-destructive-light");
      });

      it("returns warning class for awaiting_input", () => {
        expect(DashboardDetailModel.getTaskStatusClasses("awaiting_input")).toContain("bg-warning-light");
        expect(DashboardDetailModel.getTaskStatusClasses("waiting_input")).toContain("bg-warning-light");
      });

      it("returns muted class for unknown status", () => {
        expect(DashboardDetailModel.getTaskStatusClasses("unknown")).toContain("bg-surface-muted");
      });

      it("handles undefined status", () => {
        expect(DashboardDetailModel.getTaskStatusClasses(undefined)).toContain("bg-surface-muted");
      });
    });

    describe("getValidationStatus", () => {
      it("returns na for non-validation tasks", () => {
        expect(DashboardDetailModel.getValidationStatus({ plugin_id: "backup" }).level).toBe("na");
      });

      it("returns fail for FAIL status", () => {
        const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "FAIL" });
        expect(r.level).toBe("fail");
        expect(r.label).toBe("FAIL");
      });

      it("returns warn for WARN status", () => {
        const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "WARN" });
        expect(r.level).toBe("warn");
      });

      it("returns pass for PASS status", () => {
        const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "PASS" });
        expect(r.level).toBe("pass");
        expect(r.icon).toBe("OK");
      });

      it("returns unknown for unrecognized status", () => {
        const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "ERROR" });
        expect(r.level).toBe("unknown");
      });

      it("handles missing validation_status", () => {
        const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation" });
        expect(r.level).toBe("unknown");
      });
    });

    describe("getValidationStatusClasses", () => {
      it("returns destructive for fail", () => {
        expect(DashboardDetailModel.getValidationStatusClasses("fail")).toContain("bg-destructive-light");
      });
      it("returns warning for warn", () => {
        expect(DashboardDetailModel.getValidationStatusClasses("warn")).toContain("bg-warning-light");
      });
      it("returns success for pass", () => {
        expect(DashboardDetailModel.getValidationStatusClasses("pass")).toContain("bg-success-light");
      });
      it("returns muted for unknown", () => {
        expect(DashboardDetailModel.getValidationStatusClasses("unknown")).toContain("bg-surface-muted");
      });
      it("returns page bg for na", () => {
        expect(DashboardDetailModel.getValidationStatusClasses("na")).toContain("bg-surface-page");
      });
    });

    describe("formatDate", () => {
      it("returns dash for falsy input", () => {
        expect(DashboardDetailModel.formatDate(null)).toBe("-");
        expect(DashboardDetailModel.formatDate(undefined)).toBe("-");
        expect(DashboardDetailModel.formatDate("")).toBe("-");
      });

      it("returns formatted date for valid input", () => {
        const result = DashboardDetailModel.formatDate("2024-06-10T12:30:00Z");
        expect(result).toBeTruthy();
        expect(result).not.toBe("-");
      });

      it("returns dash for invalid date string", () => {
        expect(DashboardDetailModel.formatDate("not-a-date")).toBe("-");
      });
    });
  });
});
// #endregion DashboardDetailModelTests
