// #region DatasetReviewModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dataset-review]
// @BRIEF L1 unit tests for DatasetReviewModel — no DOM render, mocks all dependencies.
// @RELATION BINDS_TO -> [DatasetReviewModel]
// @TEST_INVARIANT: sessionVersion-gating -> VERIFIED_BY: [test_sync_from_store_ignores_stale_updates]
// @TEST_INVARIANT: bootstrapping-blocks -> VERIFIED_BY: [test_bootstrap_sets_state]
import { describe, it, expect, vi, beforeEach } from "vitest";

// ── Mocks (must be before imports) ───────────────────────────────

vi.mock("$lib/api.js", () => ({
  api: {
    postApi: vi.fn(),
    fetchApi: vi.fn(),
  },
}));

vi.mock("$lib/stores/environmentContext.svelte.js", () => ({
  initializeEnvironmentContext: vi.fn().mockResolvedValue(undefined),
  environmentContextStore: {
    value: { environments: [], selectedEnvId: "" },
    subscribe: vi.fn(() => () => {}),
  },
}));

vi.mock("$lib/stores/datasetReviewSession.svelte.js", () => ({
  datasetReviewSessionStore: {
    value: { focusTarget: null, session: null },
    subscribe: vi.fn(() => () => {}),
  },
}));

vi.mock("$lib/api/dataset-review/useReviewSession.js", () => ({
  useReviewSession: () => ({
    loadSessionDetail: vi.fn((sessionId) => {
      if (!sessionId) {
        return Promise.resolve({ session: null, clarificationState: null, loadError: "", previewUiState: "" });
      }
      return Promise.resolve({
        session: {
          session_id: sessionId,
          dataset_ref: "public.sales",
          readiness_state: "partially_ready",
          recommended_action: "review_documentation",
          session_version: 3,
          version: 3,
          findings: [{ severity: "blocking", resolution_state: "open", finding_id: "f1" }],
          imported_filters: [],
          execution_mappings: [{ mapping_id: "m1", requires_explicit_approval: true, approval_state: "pending" }],
          previews: [{ preview_id: "p1", preview_status: "ready" }],
          profile: { dataset_name: "Sales" },
        },
        clarificationState: {
          clarification_session: { remaining_count: 2, resolved_count: 1 },
          current_question: { question_id: "q1", question_text: "Which label?" },
        },
        loadError: "",
        previewUiState: "",
      });
    }),
    handleResumeSession: vi.fn(() => Promise.resolve({ loadError: "" })),
    updateSessionLifecycle: vi.fn(() => Promise.resolve({ loadError: "" })),
    exportArtifact: vi.fn(() => Promise.resolve({ exportMessage: "Exported", isExporting: false })),
    handlePreviewUpdated: vi.fn((_session, _result) => ({
      session: null,
      previewUiState: "",
    })),
    handleSemanticUpdated: vi.fn((_session, _result) => ({ session: null })),
    handleMappingUpdated: vi.fn((_session, _result) => ({ session: null, previewUiState: "" })),
    handleLaunchUpdated: vi.fn((result) => ({ launchResult: result || null })),
    handleSectionJump: vi.fn(),
    handleAskAiContext: vi.fn(),
    primaryActionHandler: vi.fn(),
    buildSessionUrl: vi.fn((id) => `/datasets/review/${encodeURIComponent(String(id))}`),
  }),
}));

vi.mock("$lib/helpers/review-workspace-helpers.js", () => ({
  buildImportMilestones: vi.fn(() => []),
  buildWorkspaceLaunchBlockers: vi.fn(() => []),
  buildAssistantSeedPrompt: vi.fn(() => "Assistant prompt"),
  buildAssistantContextPrompt: vi.fn(() => "Context prompt"),
  getWorkspaceStateLabel: vi.fn((_t, state) => state || "empty"),
  getRecommendedActionLabel: vi.fn((_t, action) => action || "review"),
  getPrimaryActionCtaLabel: vi.fn(() => "Continue"),
  stringifyValue: vi.fn((_t, val) => String(val)),
}));

vi.mock("$lib/i18n/index.svelte.js", () => ({
  getT: () => ({
    dataset_review: {
      source: { title: "Start dataset review", recognized_link_hint: "Link recognized", dataset_selection_acknowledged: "Source submitted" },
      workspace: {
        eyebrow: "Dataset review", title: "Workspace", description: "Review",
        loading: "Loading", empty_state_title: "Empty",
        readiness: { partially_ready: "Partially ready" },
        state: { empty: "empty", importing: "importing", review: "review" },
      },
      findings: { blocking_title: "Blocking" },
    },
    common: { error: "Error" },
  }),
}));

import { DatasetReviewModel } from "../DatasetReviewModel.svelte.ts";
import { api } from "$lib/api.js";
import { initializeEnvironmentContext } from "$lib/stores/environmentContext.svelte.js";
import { buildWorkspaceLaunchBlockers } from "$lib/helpers/review-workspace-helpers.js";

// ── Suite ────────────────────────────────────────────────────────

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

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

  // ═══════════════════════════════════════════════════════════════
  // Initial state
  // ═══════════════════════════════════════════════════════════════
  describe("initial state", () => {
    it("starts in bootstrapping/empty state", () => {
      expect(model.isBootstrapping).toBe(true);
      expect(model.currentWorkspaceState).toBe("empty");
      expect(model.session).toBeNull();
      expect(model.loadError).toBe("");
      expect(model.submitError).toBe("");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: bootstrapping blocks until session loaded
  // ═══════════════════════════════════════════════════════════════
  describe("bootstrap", () => {
    it("loads session and sets isBootstrapping to false", async () => {
      await model.bootstrap("session-1");

      expect(model.isBootstrapping).toBe(false);
      expect(model.session).not.toBeNull();
      expect(model.session?.session_id).toBe("session-1");
      expect(model.currentWorkspaceState).toBe("review");
    });

    it("handles empty sessionId gracefully", async () => {
      await model.bootstrap("");

      expect(model.isBootstrapping).toBe(false);
      expect(model.session).toBeNull();
      expect(model.loadError).toBe("");
      expect(model.currentWorkspaceState).toBe("empty");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleSourceSubmit
  // ═══════════════════════════════════════════════════════════════
  describe("handleSourceSubmit", () => {
    it("returns sessionId on success", async () => {
      vi.mocked(api.postApi).mockResolvedValue({ session_id: "session-new" });

      const result = await model.handleSourceSubmit({ source_kind: "superset_link" });

      expect(result).toBe("session-new");
      expect(model.isSubmitting).toBe(false);
      expect(model.session?.session_id).toBe("session-new");
    });

    it("returns null on API failure", async () => {
      vi.mocked(api.postApi).mockRejectedValue(new Error("Network error"));

      const result = await model.handleSourceSubmit({ source_kind: "manual" });

      expect(result).toBeNull();
      expect(model.isSubmitting).toBe(false);
      expect(model.submitError).toBeTruthy();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: sessionVersion gates stale updates
  // ═══════════════════════════════════════════════════════════════
  describe("syncFromStore", () => {
    it("updates session when version is newer", async () => {
      await model.bootstrap("session-1");
      expect(model.session?.session_version).toBe(3);

      const newerSession = {
        session_id: "session-1",
        session_version: 4,
        dataset_ref: "public.sales",
      };
      model.syncFromStore(newerSession as any);

      expect(model.session?.session_version).toBe(4);
    });

    it("ignores session with same version", async () => {
      await model.bootstrap("session-1");
      const sameVersionSession = {
        session_id: "session-1",
        session_version: 3,
        dataset_ref: "public.sales",
      };
      model.syncFromStore(sameVersionSession as any);

      expect(model.session?.session_version).toBe(3);
      // session reference not replaced
    });

    it("ignores session with different ID", async () => {
      await model.bootstrap("session-1");
      const differentSession = {
        session_id: "session-other",
        session_version: 4,
      };
      model.syncFromStore(differentSession as any);

      expect(model.session?.session_id).toBe("session-1");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // State transitions
  // ═══════════════════════════════════════════════════════════════
  describe("workspace state transitions", () => {
    it("transitions from empty to importing during submit", () => {
      // Before submit: no session, not submitting → empty
      expect(model.currentWorkspaceState).toBe("empty");
    });

    it("transitions to review after bootstrap", async () => {
      await model.bootstrap("session-1");
      expect(model.currentWorkspaceState).toBe("review");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Derived values
  // ═══════════════════════════════════════════════════════════════
  describe("derived values", () => {
    it("computes blockingCount from findings", async () => {
      await model.bootstrap("session-1");
      expect(model.findings).toHaveLength(1);
      expect(model.blockingCount).toBe(1);
    });

    it("computes previewGateState", async () => {
      await model.bootstrap("session-1");
      expect(model.previewGateState).toBe("ready");
    });

    it("exportDisabled is true when no session", () => {
      expect(model.exportDisabled).toBe(true);
    });

    it("exportDisabled is false with loaded session", async () => {
      await model.bootstrap("session-1");
      expect(model.exportDisabled).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleExportArtifact
  // ═══════════════════════════════════════════════════════════════
  describe("handleExportArtifact", () => {
    it("stores export message on success", async () => {
      await model.bootstrap("session-1");

      expect(model.exportMessage).toBe("");

      await model.handleExportArtifact("documentation", "json");

      expect(model.exportMessage).toBe("Exported");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleLaunchUpdated
  // ═══════════════════════════════════════════════════════════════
  describe("handleLaunchUpdated", () => {
    it("stores launch result when provided", () => {
      model.handleLaunchUpdated({ task_id: "task-1", success: true });
      expect(model.launchResult).toEqual({ task_id: "task-1", success: true });
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleResumeSession / updateSessionLifecycle
  // ═══════════════════════════════════════════════════════════════
  describe("handleResumeSession", () => {
    it("resumes session and updates loadError on failure", async () => {
      const { useReviewSession } = await import("$lib/api/dataset-review/useReviewSession.js");
      const mockSession = vi.mocked(useReviewSession);
      // The mock returns { loadError: "" } by default — we need error path
      // Bootstrap first to set session
      await model.bootstrap("session-1");
      expect(model.loadError).toBe("");
      await model.handleResumeSession();
      expect(model.loadError).toBe("");
    });

    it("returns early when no session_id", async () => {
      const { useReviewSession } = await import("$lib/api/dataset-review/useReviewSession.js");
      const mockSession = vi.mocked(useReviewSession);
      await model.handleResumeSession();
      expect(model.loadError).toBe("");
    });
  });

  describe("updateSessionLifecycle", () => {
    it("returns early when no session_id", async () => {
      await model.updateSessionLifecycle("paused");
      expect(model.loadError).toBe("");
    });

    it("updates lifecycle for bootstrapped session", async () => {
      await model.bootstrap("session-1");
      await model.updateSessionLifecycle("paused");
      expect(model.loadError).toBe("");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handlePreviewUpdated / handleSemanticUpdated / handleMappingUpdated
  // ═══════════════════════════════════════════════════════════════
  describe("handlePreviewUpdated", () => {
    it("merges preview into session", () => {
      model.session = { session_id: "s1", previews: [] } as any;
      model.handlePreviewUpdated({ preview_id: "p1", preview_status: "ready" });
      expect(model.session).not.toBeNull();
    });
  });

  describe("handleSemanticUpdated", () => {
    it("merges semantic fields into session", () => {
      model.session = { session_id: "s1" } as any;
      model.handleSemanticUpdated({ semantic_fields: ["f1"] });
      expect(model.session).not.toBeNull();
    });
  });

  describe("handleMappingUpdated", () => {
    it("merges mapping into session", () => {
      model.session = { session_id: "s1" } as any;
      model.handleMappingUpdated({ mappings: [{ m: "m1" }] });
      expect(model.session).not.toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // buildAssistantContextPrompt / buildSessionUrl
  // ═══════════════════════════════════════════════════════════════
  describe("static utilities", () => {
    it("buildSessionUrl encodes sessionId", () => {
      const url = DatasetReviewModel.buildSessionUrl("s1");
      expect(url).toBe("/datasets/review/s1");
    });

    it("buildAssistantContextPrompt calls helper", () => {
      const result = model.buildAssistantContextPrompt({ section: "test" });
      expect(typeof result).toBe("string");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Derived — additional edge cases
  // ═══════════════════════════════════════════════════════════════
  describe("derived values — additional", () => {
    it("profile returns null when no session", () => {
      expect(model.profile).toBeNull();
    });

    it("latestPreview returns last preview", async () => {
      await model.bootstrap("session-1");
      expect(model.latestPreview?.preview_status).toBe("ready");
    });

    it("warningCount counts warnings", async () => {
      await model.bootstrap("session-1");
      expect(model.warningCount).toBe(0);
      expect(model.infoCount).toBe(0);
    });

    it("unresolvedBlockingFindingsCount counts unresolved blockers", async () => {
      await model.bootstrap("session-1");
      // session has 1 blocking finding with resolution_state "open" (not resolved/approved)
      expect(model.unresolvedBlockingFindingsCount).toBe(1);
    });

    it("pendingApprovalMappingsCount counts pending mappings", async () => {
      await model.bootstrap("session-1");
      expect(model.pendingApprovalMappingsCount).toBe(1);
    });

    it("saveDisabled is true when no session", () => {
      expect(model.saveDisabled).toBe(true);
    });

    it("launchDisabled is true when no session", () => {
      expect(model.launchDisabled).toBe(true);
    });

    it("clarificationSession returns null when no clarificationState", () => {
      expect(model.clarificationSession).toBeNull();
    });

    it("clarificationRemainingCount defaults to 0", () => {
      expect(model.clarificationRemainingCount).toBe(0);
    });

    it("clarificationTotalCount sums remaining and resolved", async () => {
      await model.bootstrap("session-1");
      expect(model.clarificationTotalCount).toBe(3);
    });

    it("assistantPromptNonce increments", () => {
      const n = model.assistantPromptNonce;
      model.assistantPromptNonce = n + 1;
      expect(model.assistantPromptNonce).toBe(n + 1);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // handleSourceSubmit — additional coverage
  // ═══════════════════════════════════════════════════════════════
  describe("handleSourceSubmit — extended", () => {
    it("sets intakeAcknowledgment for superset_link source", async () => {
      vi.mocked(api.postApi).mockResolvedValue({ session_id: "s-new" });
      await model.handleSourceSubmit({ source_kind: "superset_link" });
      expect(model.intakeAcknowledgment).toBeTruthy();
    });

    it("sets intakeAcknowledgment for manual source", async () => {
      vi.mocked(api.postApi).mockResolvedValue({ session_id: "s-new" });
      await model.handleSourceSubmit({ source_kind: "manual" });
      expect(model.intakeAcknowledgment).toBeTruthy();
    });

    it("returns null when API returns no session_id", async () => {
      vi.mocked(api.postApi).mockResolvedValue({});
      const result = await model.handleSourceSubmit({ source_kind: "manual" });
      expect(result).toBeNull();
      expect(model.submitError).toBeTruthy();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // syncFromStore — edge cases
  // ═══════════════════════════════════════════════════════════════
  describe("syncFromStore — edge cases", () => {
    it("returns early when extSession is null", () => {
      const before = model.session;
      model.syncFromStore(null);
      expect(model.session).toBe(before);
    });

    it("returns early when extSession.session_id is falsy", () => {
      const before = model.session;
      model.syncFromStore({} as any);
      expect(model.session).toBe(before);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Derived — additional coverage
  // ═══════════════════════════════════════════════════════════════
  describe("derived — extended coverage", () => {
    it("reads importedFilters, latestRunContext, clarificationCurrentQuestion", async () => {
      await model.bootstrap("session-1");
      expect(Array.isArray(model.importedFilters)).toBe(true);
      expect(model.latestRunContext).toBeNull();
      expect(model.clarificationCurrentQuestion).not.toBeNull();
    });

    it("reads readinessLabel and workspaceFocusTarget", async () => {
      await model.bootstrap("session-1");
      expect(typeof model.readinessLabel).toBe("string");
      expect(model.workspaceFocusTarget).toBeNull();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Derived helpers
  // ═══════════════════════════════════════════════════════════════
  describe("derived helpers", () => {
    it("readinessLabel, importMilestones, launchBlockers, seedPrompt are defined", async () => {
      await model.bootstrap("session-1");
      expect(typeof model.readinessLabel).toBe("string");
      expect(Array.isArray(model.importMilestones)).toBe(true);
      expect(Array.isArray(model.workspaceLaunchBlockers)).toBe(true);
      expect(typeof model.assistantSeedPrompt).toBe("string");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // Handler — update branches
  // ═══════════════════════════════════════════════════════════════
  describe("handler update branches", () => {
    it("handlePreviewUpdated updates session when result provided", () => {
      model.session = { session_id: "s1" } as any;
      model._reviewSession.handlePreviewUpdated = vi.fn(() => ({
        session: { session_id: "s1", previews: [] },
        previewUiState: "ready",
      }));
      model.handlePreviewUpdated({ preview_id: "p1" });
      expect(model.session?.previews).toEqual([]);
      expect(model.previewUiState).toBe("ready");
    });

    it("handleSemanticUpdated updates session when result provided", () => {
      model.session = { session_id: "s1" } as any;
      model._reviewSession.handleSemanticUpdated = vi.fn(() => ({
        session: { session_id: "s1", semantic_fields: ["f1"] },
      }));
      model.handleSemanticUpdated({ semantic_fields: ["f1"] });
      expect(Array.isArray((model.session as any)?.semantic_fields)).toBe(true);
    });

    it("handleMappingUpdated updates session and previewUiState", () => {
      model.session = { session_id: "s1" } as any;
      model._reviewSession.handleMappingUpdated = vi.fn(() => ({
        session: { session_id: "s1", execution_mappings: [] },
        previewUiState: "updated",
      }));
      model.handleMappingUpdated({ mappings: [] });
      expect(model.previewUiState).toBe("updated");
    });

    it("handleLaunchUpdated stores launchResult", () => {
      model._reviewSession.handleLaunchUpdated = vi.fn(() => ({
        launchResult: { task_id: "t1" },
      }));
      model.handleLaunchUpdated({ task_id: "t1" });
      expect(model.launchResult).toEqual({ task_id: "t1" });
    });

    it("handleResumeSession sets loadError on failure", async () => {
      await model.bootstrap("session-1");
      model._reviewSession.handleResumeSession = vi.fn(() => Promise.resolve({ loadError: "Resume error" }));
      await model.handleResumeSession();
      expect(model.loadError).toBe("Resume error");
    });

    it("handleExportArtifact stores message when session exists", async () => {
      await model.bootstrap("session-1");
      await model.handleExportArtifact("doc", "json");
      expect(model.exportMessage).toBe("Exported");
    });

    it("handleExportArtifact returns early without session_id", async () => {
      model.session = { session_id: null } as any;
      const result = model.handleExportArtifact("doc", "json");
      // Should return undefined early, no API call made
      // (the method returns void, but the guard stops execution)
    });

    it("handlePreviewUpdated does nothing when updated.session is null", () => {
      model.session = { session_id: "s1" } as any;
      model._reviewSession.handlePreviewUpdated = vi.fn(() => ({
        session: null, previewUiState: "",
      }));
      model.handlePreviewUpdated({ preview_id: "p1" });
      expect(model.previewUiState).toBe("");
    });
  });
});
// #endregion DatasetReviewModelTests
