// #region DatasetsHubModelTests [C:3] [TYPE Module] [SEMANTICS test,model,datasets-hub]
// @BRIEF L1 unit tests for DatasetsHubModel — no DOM render, mocks API.
// @RELATION BINDS_TO -> [Datasets.HubModel]
// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_set_filter_resets_page, test_set_search_resets_page]
// @TEST_INVARIANT: detail-panel-gate -> VERIFIED_BY: [test_detail_open_close, test_select_dataset_loads_detail]
// @TEST_INVARIANT: selection-cleaned-on-load -> VERIFIED_BY: [test_selection_cleaned_after_load]
// @TEST_EDGE: missing_field -> datasets response has no items
// @TEST_EDGE: invalid_type -> API returns unexpected shape
// @TEST_EDGE: external_fail -> API throws on loadDatasets
import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("$lib/api.js", () => ({
  api: {
    getDatasets: vi.fn(),
    getDatasetDetail: vi.fn(),
    postApi: vi.fn(),
    getEnvironmentDatabases: vi.fn(),
  },
  requestApi: vi.fn(),
}));
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));
vi.mock("$lib/utils/debounce.js", () => ({ debounce: vi.fn((fn) => fn) }));

import { DatasetsHubModel } from "../DatasetsHubModel.svelte.ts";
import { api } from "$lib/api.js";

// ── Helpers ──────────────────────────────────────────────────────

const makeDatasetsResponse = (overrides: Record<string, unknown> = {}) => ({
  datasets: [
    { id: "ds-1", table_name: "sales", schema: "public", database: "prod", column_count: 10, metric_count: 5, mapped_fields: { total: 10, mapped: 7 }, last_task: null },
    { id: "ds-2", table_name: "users", schema: "public", database: "prod", column_count: 8, metric_count: 2, mapped_fields: null, last_task: { status: "completed", task_id: "t-1" } },
  ],
  total: 2,
  total_pages: 1,
  page: 1,
  stats: { total: 20, mapped: 15 },
  ...overrides,
});

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

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

  // ═══════════════════════════════════════════════════════════════
  // Initial state
  // ═══════════════════════════════════════════════════════════════
  describe("initial state", () => {
    it("starts with default values", () => {
      expect(model.screenState).toBe("idle");
      expect(model.datasets).toEqual([]);
      expect(model.selectedIds.size).toBe(0);
      expect(model.selectedEnv).toBeNull();
      expect(model.page).toBe(1);
      expect(model.searchQuery).toBe("");
      expect(model.activeFilter).toBeNull();
      expect(model.detailData).toBeNull();
      expect(model.selectedDatasetId).toBeNull();
      expect(model.showMapColumnsModal).toBe(false);
      expect(model.showGenerateDocsModal).toBe(false);
    });

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

    it("derived hasSelection is false when selection is empty", () => {
      expect(model.hasSelection).toBe(false);
    });

    it("derived hasSelection is true when selection has items", () => {
      model.selectedIds = new Set(["ds-1"]);
      expect(model.hasSelection).toBe(true);
    });

    it("derived selectionCount returns correct count", () => {
      expect(model.selectionCount).toBe(0);
      model.selectedIds = new Set(["ds-1", "ds-2"]);
      expect(model.selectionCount).toBe(2);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadDatasets
  // ═══════════════════════════════════════════════════════════════
  describe("loadDatasets", () => {
    it("returns early when selectedEnv is null", async () => {
      await model.loadDatasets();
      expect(api.getDatasets).not.toHaveBeenCalled();
    });

    it("loads datasets on success", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse());

      await model.loadDatasets();

      expect(api.getDatasets).toHaveBeenCalledWith("env-1", expect.objectContaining({ page: 1 }));
      expect(model.datasets).toHaveLength(2);
      expect(model.screenState).toBe("loaded");
      expect(model.stats).toEqual({ total: 20, mapped: 15 });
      expect(model.dsError).toBeNull();
    });

    it("sets screenState to empty when no datasets returned", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse({ datasets: [] }));

      await model.loadDatasets();

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

    it("sets error state on API failure", async () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDatasets).mockRejectedValue(new Error("Server error"));

      await model.loadDatasets();

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

    // @INVARIANT: orphan selection IDs cleaned after load
    it("cleans orphan selectedIds that are not in loaded datasets", async () => {
      model.selectedEnv = "env-1";
      model.selectedIds = new Set(["ds-1", "ds-orphan"]);
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse());

      await model.loadDatasets();

      expect(model.selectedIds.has("ds-1")).toBe(true);
      expect(model.selectedIds.has("ds-orphan")).toBe(false);
      expect(model.selectedIds.size).toBe(1);
    });

    it("passes search and filter params", async () => {
      model.selectedEnv = "env-1";
      model.searchQuery = "sales";
      model.activeFilter = "mapped";
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse());

      await model.loadDatasets();

      expect(api.getDatasets).toHaveBeenCalledWith("env-1", expect.objectContaining({
        search: "sales",
        filter: "mapped",
      }));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Changing filter or search resets pagination
  // ═══════════════════════════════════════════════════════════════
  describe("filter resets pagination", () => {
    it("setFilter resets page to 1", () => {
      model.page = 5;
      model.selectedEnv = "env-1";
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse());

      model.setFilter("mapped");

      expect(model.page).toBe(1);
      expect(model.activeFilter).toBe("mapped");
      expect(api.getDatasets).toHaveBeenCalled();
    });

    it("setSearch resets page to 1 via debounce", () => {
      model.page = 3;
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse());
      model.selectedEnv = "env-1";

      model.setSearch("test query");

      // Debounce mock calls fn immediately
      expect(model.searchQuery).toBe("test query");
      expect(model.page).toBe(1);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // setPage
  // ═══════════════════════════════════════════════════════════════
  describe("setPage", () => {
    it("updates page and reloads", () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDatasets).mockResolvedValue(makeDatasetsResponse());

      model.setPage(3);

      expect(model.page).toBe(3);
      expect(api.getDatasets).toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Selection
  // ═══════════════════════════════════════════════════════════════
  describe("selection", () => {
    it("toggleSelect adds single id", () => {
      model.toggleSelect({ id: "ds-1" } as any, true);
      expect(model.selectedIds.has("ds-1")).toBe(true);
    });

    it("toggleSelect removes single id", () => {
      model.selectedIds = new Set(["ds-1", "ds-2"]);
      model.toggleSelect({ id: "ds-1" } as any, false);
      expect(model.selectedIds.has("ds-1")).toBe(false);
      expect(model.selectedIds.size).toBe(1);
    });

    it("toggleSelect with 'all' selects all datasets", () => {
      model.datasets = [
        { id: "ds-1" } as any,
        { id: "ds-2" } as any,
        { id: "ds-3" } as any,
      ];
      model.toggleSelect({} as any, "all");
      expect(model.selectedIds.size).toBe(3);
    });

    it("toggleSelect with 'clear-all' empties all", () => {
      model.selectedIds = new Set(["ds-1", "ds-2"]);
      model.toggleSelect({} as any, "clear-all");
      expect(model.selectedIds.size).toBe(0);
    });

    it("selectAll delegates to toggleSelect", () => {
      model.datasets = [{ id: "ds-1" } as any, { id: "ds-2" } as any];
      model.selectAll();
      expect(model.selectedIds.size).toBe(2);
    });

    it("clearAll empties selection", () => {
      model.selectedIds = new Set(["ds-1", "ds-2"]);
      model.clearAll();
      expect(model.selectedIds.size).toBe(0);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Detail panel open/close
  // ═══════════════════════════════════════════════════════════════
  describe("detail panel", () => {
    it("selectDataset sets selectedDatasetId and loads detail", () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getDatasetDetail).mockResolvedValue({ table_name: "sales" });

      model.selectDataset("ds-1");

      expect(model.selectedDatasetId).toBe("ds-1");
      expect(model.detailLoading).toBe(true);
      expect(api.getDatasetDetail).toHaveBeenCalledWith("env-1", "ds-1");
    });

    it("closeDetail clears everything", () => {
      model.selectedDatasetId = "ds-1";
      model.detailData = { table_name: "test" } as any;
      model.detailError = "error";

      model.closeDetail();

      expect(model.selectedDatasetId).toBeNull();
      expect(model.detailData).toBeNull();
      expect(model.detailError).toBeNull();
    });

    it("loadDetail fetches dataset and normalizes schema", async () => {
      vi.mocked(api.getDatasetDetail).mockResolvedValue({
        table_name: "sales",
        schema_name: "custom_schema",
      });

      await model.loadDetail("ds-1");

      expect(model.detailData).toEqual(
        expect.objectContaining({ schema: "custom_schema" }),
      );
      expect(model.detailLoading).toBe(false);
    });

    it("loadDetail handles API failure", async () => {
      vi.mocked(api.getDatasetDetail).mockRejectedValue(new Error("Detail fail"));

      await model.loadDetail("ds-1");

      expect(model.detailError).toBe("Detail fail");
      expect(model.detailLoading).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleAction
  // ═══════════════════════════════════════════════════════════════
  describe("handleAction", () => {
    it("opens map-columns modal with defaults", () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);

      model.handleAction({ id: "ds-1" } as any, "map_columns");

      expect(model.showMapColumnsModal).toBe(true);
      expect(model.mapSourceType).toBe("sqllab");
      expect(api.getEnvironmentDatabases).toHaveBeenCalledWith("env-1");
    });

    it("opens generate-docs modal with defaults", () => {
      model.handleAction({ id: "ds-1" } as any, "generate_docs");

      expect(model.showGenerateDocsModal).toBe(true);
      expect(model.llmProvider).toBe("");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Bulk actions
  // ═══════════════════════════════════════════════════════════════
  describe("handleBulkMapColumns", () => {
    it("returns early when no selection", async () => {
      await model.handleBulkMapColumns();
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("returns early when sqllab type and no databaseId", async () => {
      model.selectedIds = new Set(["ds-1"]);
      model.mapSourceType = "sqllab";
      model.mapDatabaseId = null;

      await model.handleBulkMapColumns();

      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("returns early when xlsx type and no file data", async () => {
      model.selectedIds = new Set(["ds-1"]);
      model.mapSourceType = "xlsx";
      model.mapFileData = [];

      await model.handleBulkMapColumns();

      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("posts map-columns request on success", async () => {
      model.selectedEnv = "env-1";
      model.selectedIds = new Set(["ds-1"]);
      model.mapSourceType = "sqllab";
      model.mapDatabaseId = "db-1";
      model.mapSqlQuery = "SELECT * FROM t";
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });

      await model.handleBulkMapColumns();

      expect(api.postApi).toHaveBeenCalledWith("/datasets/map-columns", {
        env_id: "env-1",
        dataset_ids: ["ds-1"],
        source_type: "sqllab",
        database_id: "db-1",
        sql_query: "SELECT * FROM t",
        file_data: undefined,
      });
      expect(model.showMapColumnsModal).toBe(false);
      expect(model.selectedIds.size).toBe(0);
    });
  });

  describe("handleBulkGenerateDocs", () => {
    it("returns early when no selection", async () => {
      await model.handleBulkGenerateDocs();
      expect(api.postApi).not.toHaveBeenCalled();
    });

    it("posts generate-docs request", async () => {
      model.selectedEnv = "env-1";
      model.selectedIds = new Set(["ds-1", "ds-2"]);
      model.llmProvider = "openai";
      model.llmOptions = { model: "gpt-4" };
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-2" });

      await model.handleBulkGenerateDocs();

      expect(api.postApi).toHaveBeenCalledWith("/datasets/generate-docs", {
        env_id: "env-1",
        dataset_ids: ["ds-1", "ds-2"],
        llm_provider: "openai",
        llm_options: { model: "gpt-4" },
      });
      expect(model.showGenerateDocsModal).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadDetail guard
  // ═══════════════════════════════════════════════════════════════
  describe("loadDetail guard", () => {
    it("returns early when datasetId is empty", async () => {
      await model.loadDetail("");
      expect(api.getDatasetDetail).not.toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // selectDataset sets mobileSlideOpen on mobile
  // ═══════════════════════════════════════════════════════════════
  describe("selectDataset", () => {
    it("sets mobileSlideOpen when isMobile", () => {
      model.selectedEnv = "env-1";
      model.isMobile = true;
      vi.mocked(api.getDatasetDetail).mockResolvedValue({ table_name: "test" });
      model.selectDataset("ds-1");
      expect(model.mobileSlideOpen).toBe(true);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleAction map_columns error path for getEnvironmentDatabases
  // ═══════════════════════════════════════════════════════════════
  describe("handleAction map_columns error", () => {
    it("handles getEnvironmentDatabases failure gracefully", () => {
      model.selectedEnv = "env-1";
      vi.mocked(api.getEnvironmentDatabases).mockRejectedValue(new Error("DB fail"));
      model.handleAction({ id: "ds-1" } as any, "map_columns");
      expect(model.showMapColumnsModal).toBe(true);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleBulkMapColumns — xlsx with file data
  // ═══════════════════════════════════════════════════════════════
  describe("handleBulkMapColumns xlsx", () => {
    it("sends file_data with xlsx source", async () => {
      model.selectedEnv = "env-1";
      model.selectedIds = new Set(["ds-1"]);
      model.mapSourceType = "xlsx";
      model.mapFileData = [{ name: "data.xlsx" }];
      vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });
      await model.handleBulkMapColumns();
      expect(api.postApi).toHaveBeenCalledWith("/datasets/map-columns", expect.objectContaining({
        file_data: "data.xlsx",
        source_type: "xlsx",
      }));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleBulkMapColumns / handleBulkGenerateDocs — error paths
  // ═══════════════════════════════════════════════════════════════
  describe("bulk action errors", () => {
    it("handleBulkMapColumns handles API error gracefully", async () => {
      model.selectedEnv = "env-1";
      model.selectedIds = new Set(["ds-1"]);
      model.mapSourceType = "sqllab";
      model.mapDatabaseId = "db-1";
      vi.mocked(api.postApi).mockRejectedValue(new Error("Map error"));
      await model.handleBulkMapColumns();
      // no exception
    });

    it("handleBulkGenerateDocs handles API error gracefully", async () => {
      model.selectedEnv = "env-1";
      model.selectedIds = new Set(["ds-1"]);
      vi.mocked(api.postApi).mockRejectedValue(new Error("Doc error"));
      await model.handleBulkGenerateDocs();
      // no exception
    });
  });
});

// #endregion DatasetsHubModelTests
