// frontend/src/lib/models/__tests__/CommitModel.test.ts
// #region CommitModelTests [C:3] [TYPE Module] [SEMANTICS test,model,commit]
// @BRIEF L1 unit tests for CommitModel — no DOM render, mocks API + toast + i18n.
// @RELATION BINDS_TO -> [CommitModel]
import { describe, it, expect, vi, beforeEach } from "vitest";

// ── Mocks ────────────────────────────────────────────────────────
vi.mock("../../../services/gitService.js", () => ({
  gitService: {
    getStatus: vi.fn(),
    getDiff: vi.fn(),
    commit: vi.fn(),
    push: vi.fn(),
  },
}));

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

vi.mock('$lib/toasts.svelte.js', () => ({
  addToast: vi.fn(),
}));

vi.mock('$lib/i18n/index.svelte.js', () => ({
  t: { subscribe: vi.fn() }, getT: vi.fn(() => ({})),
  _: vi.fn(), getT: vi.fn(() => ({})),
}));

vi.mock("svelte/store", () => ({
  get: vi.fn(() => ({ git: {} })),
}));

import { CommitModel } from "../CommitModel.svelte.ts";
import { gitService } from "../../../services/gitService.js";
import { api } from "$lib/api.js";

// ── Types ─────────────────────────────────────────────────────────

interface StatusFixture {
  is_dirty: boolean;
  current_branch: string;
  staged_files: string[];
  modified_files: string[];
  untracked_files: string[];
  [key: string]: unknown;
}

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

/** Create a model with default constructor values. */
function createModel(overrides: Partial<{ dashboardId: string; envId: string | null; show: boolean }> = {}): CommitModel {
  return new CommitModel({
    dashboardId: "test-dashboard",
    envId: "env-123",
    show: true,
    ...overrides,
  });
}

/** Minimal git status payload. */
function makeStatus(overrides: Partial<StatusFixture> = {}): StatusFixture {
  return {
    is_dirty: true,
    current_branch: "main",
    staged_files: ["file1.py"],
    modified_files: [],
    untracked_files: [],
    ...overrides,
  };
}

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

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

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loadStatus calls gitService
  // ═══════════════════════════════════════════════════════════════
  describe("loadStatus", () => {
    it("fetches status and diff via gitService", async () => {
      const status = makeStatus();
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(status);
      vi.mocked(gitService.getDiff).mockResolvedValueOnce("staged diff content");
      vi.mocked(gitService.getDiff).mockResolvedValueOnce("unstaged diff content");

      await model.loadStatus();

      expect(gitService.getStatus).toHaveBeenCalledWith("test-dashboard", "env-123");
      expect(gitService.getDiff).toHaveBeenCalledWith("test-dashboard", null, true, "env-123");
      expect(gitService.getDiff).toHaveBeenCalledWith("test-dashboard", null, false, "env-123");
      expect(model.status).toEqual(status);
      expect(model.loading).toBe(false);
    });

    it("combines staged and unstaged diff", async () => {
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());
      vi.mocked(gitService.getDiff).mockResolvedValueOnce("staged content");
      vi.mocked(gitService.getDiff).mockResolvedValueOnce("unstaged content");

      await model.loadStatus();

      expect(model.diff).toContain("STAGED CHANGES");
      expect(model.diff).toContain("UNSTAGED CHANGES");
      expect(model.diff).toContain("staged content");
      expect(model.diff).toContain("unstaged content");
    });

    it("sets empty diff when there are no changes", async () => {
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());
      vi.mocked(gitService.getDiff).mockResolvedValueOnce(null);
      vi.mocked(gitService.getDiff).mockResolvedValueOnce(null);

      await model.loadStatus();

      expect(model.diff).toBe("");
    });

    it("sets error on API failure", async () => {
      vi.mocked(gitService.getStatus).mockRejectedValueOnce(new Error("Network error"));

      await model.loadStatus();

      expect(model.status).toBeNull();
      expect(model.diff).toBe("");
      expect(model.loading).toBe(false);
    });

    it("does nothing when dashboardId is empty", async () => {
      model.dashboardId = "";
      await model.loadStatus();
      expect(gitService.getStatus).not.toHaveBeenCalled();
    });

    it("does nothing when show is false", async () => {
      model.show = false;
      await model.loadStatus();
      expect(gitService.getStatus).not.toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loading lifecycle
  // ═══════════════════════════════════════════════════════════════
  describe("loading lifecycle", () => {
    it("loadStatus sets loading true then false", async () => {
      vi.mocked(gitService.getStatus).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));
      vi.mocked(gitService.getDiff).mockResolvedValueOnce("");
      vi.mocked(gitService.getDiff).mockResolvedValueOnce("");

      const promise = model.loadStatus();
      expect(model.loading).toBe(true);
      await promise;
      expect(model.loading).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleGenerateMessage
  // ═══════════════════════════════════════════════════════════════
  describe("handleGenerateMessage", () => {
    it("calls api.postApi and sets message", async () => {
      vi.mocked(api.postApi).mockResolvedValueOnce({ message: "AI generated commit message" });

      await model.handleGenerateMessage();

      expect(api.postApi).toHaveBeenCalledWith(
        expect.stringContaining("/git/repositories/test-dashboard/generate-message"),
        undefined,
        { suppressToast: true },
      );
      expect(model.message).toBe("AI generated commit message");
      expect(model.generatingMessage).toBe(false);
    });

    it("sets error on API failure", async () => {
      vi.mocked(api.postApi).mockRejectedValueOnce(new Error("Generation failed"));

      await model.handleGenerateMessage();

      expect(model.generatingMessage).toBe(false);
    });

    it("appends env_id when envId is set", async () => {
      vi.mocked(api.postApi).mockResolvedValueOnce({ message: "msg" });

      await model.handleGenerateMessage();

      expect(api.postApi).toHaveBeenCalledWith(
        expect.stringContaining("env_id=env-123"),
        undefined,
        { suppressToast: true },
      );
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: committing lifecycle
  // ═══════════════════════════════════════════════════════════════
  describe("committing lifecycle", () => {
    it("sets committing true then false", async () => {
      model.message = "fix: test";
      model.status = makeStatus();
      vi.mocked(gitService.commit).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));

      const promise = model.handleCommit();
      expect(model.committing).toBe(true);
      await promise;
      expect(model.committing).toBe(false);
    });

    it("resets committing on failure", async () => {
      model.message = "fix: test";
      model.status = makeStatus();
      vi.mocked(gitService.commit).mockRejectedValueOnce(new Error("Commit rejected"));

      await model.handleCommit();

      expect(model.committing).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleCommit calls gitService.commit
  // ═══════════════════════════════════════════════════════════════
  describe("handleCommit — API calls", () => {
    it("calls gitService.commit with correct args", async () => {
      model.message = "fix: resolve issue";
      model.status = makeStatus();
      model.autoPushAfterCommit = false;
      vi.mocked(gitService.commit).mockResolvedValueOnce({});

      await model.handleCommit();

      expect(gitService.commit).toHaveBeenCalledWith(
        "test-dashboard",
        "fix: resolve issue",
        [],
        "env-123",
      );
    });

    it("calls push when autoPushAfterCommit is true", async () => {
      model.message = "fix: auto-push";
      model.status = makeStatus();
      model.autoPushAfterCommit = true;
      vi.mocked(gitService.commit).mockResolvedValueOnce({});
      vi.mocked(gitService.push).mockResolvedValueOnce({});

      await model.handleCommit();

      expect(gitService.push).toHaveBeenCalledWith("test-dashboard", "env-123");
    });

    it("does not call push when autoPushAfterCommit is false", async () => {
      model.message = "fix: no-push";
      model.status = makeStatus();
      model.autoPushAfterCommit = false;
      vi.mocked(gitService.commit).mockResolvedValueOnce({});

      await model.handleCommit();

      expect(gitService.push).not.toHaveBeenCalled();
    });

    it("clears message and closes modal on success", async () => {
      model.message = "fix: done";
      model.status = makeStatus();
      model.autoPushAfterCommit = false;
      vi.mocked(gitService.commit).mockResolvedValueOnce({});

      await model.handleCommit();

      expect(model.message).toBe("");
      expect(model.show).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: canCommit derived
  // ═══════════════════════════════════════════════════════════════
  describe("canCommit derived", () => {
    it("is false when loading", () => {
      model.loading = true;
      model.message = "fix";
      model.status = makeStatus();
      expect(model.canCommit).toBe(false);
    });

    it("is false when committing", () => {
      model.committing = true;
      model.message = "fix";
      model.status = makeStatus();
      expect(model.canCommit).toBe(false);
    });

    it("is false when message is empty", () => {
      model.message = "";
      model.status = makeStatus();
      expect(model.canCommit).toBe(false);
    });

    it("is false when status is null", () => {
      model.message = "fix";
      model.status = null;
      expect(model.canCommit).toBe(false);
    });

    it("is false when there are no changes", () => {
      model.message = "fix";
      model.status = makeStatus({ is_dirty: false, staged_files: [] });
      expect(model.canCommit).toBe(false);
    });

    it("is true when ready to commit", () => {
      model.message = "fix: ready";
      model.status = makeStatus();
      expect(model.canCommit).toBe(true);
    });
  });
});
// #endregion CommitModelTests
