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

// ── Mocks ────────────────────────────────────────────────────────
vi.mock("../../../services/gitService.js", () => ({
  gitService: {
    getStatus: vi.fn(),
    getDiff: vi.fn(),
    sync: vi.fn(),
    pull: vi.fn(),
    push: vi.fn(),
    getHistory: 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: { repo_status: {} } })),
}));

import { GitStatusModel } from "../GitStatusModel.svelte.ts";
import { gitService } from "../../../services/gitService.js";

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

interface StatusFixture {
  sync_state: string;
  current_branch: string | null;
  has_repo: boolean;
  modified_files: string[];
  staged_files: string[];
  untracked_files: string[];
  ahead_count: number;
  behind_count: number;
  [key: string]: unknown;
}

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

/** Create a model with dashboardId and envId pre-set. */
function createModel(): GitStatusModel {
  const model = new GitStatusModel();
  model.initialize("test-dashboard", "env-123");
  return model;
}

/** Minimal git status payload. */
function makeStatus(overrides: Partial<StatusFixture> = {}): StatusFixture {
  return {
    sync_state: "SYNCED",
    current_branch: "main",
    has_repo: true,
    modified_files: [],
    staged_files: [],
    untracked_files: [],
    ahead_count: 0,
    behind_count: 0,
    ...overrides,
  };
}

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

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

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: initialize
  // ═══════════════════════════════════════════════════════════════
  describe("initialize", () => {
    it("sets dashboardId and envId", () => {
      expect(model.dashboardId).toBe("test-dashboard");
      expect(model.envId).toBe("env-123");
    });
  });

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

      await model.loadStatus();

      expect(gitService.getStatus).toHaveBeenCalledWith("test-dashboard", "env-123");
      expect(model.gitStatus).toEqual(status);
      expect(model.statusLoading).toBe(false);
    });

    it("sets currentBranch from response", async () => {
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus({ current_branch: "develop" }));

      await model.loadStatus();

      expect(model.currentBranch).toBe("develop");
    });

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

      await model.loadStatus();

      expect(model.statusError).toBe("Network error");
      expect(model.gitStatus).not.toBeNull();
      expect(model.gitStatus!.has_repo).toBe(false);
      expect(model.statusLoading).toBe(false);
    });

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: sync calls gitService.sync
  // ═══════════════════════════════════════════════════════════════
  describe("syncRepository", () => {
    it("calls gitService.sync with correct args", async () => {
      model.gitStatus = makeStatus();
      vi.mocked(gitService.sync).mockResolvedValueOnce({});
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());

      const result = await model.syncRepository();

      expect(gitService.sync).toHaveBeenCalledWith("test-dashboard", "env-123", "env-123");
      expect(result).toBe(true);
      expect(model.syncing).toBe(false);
    });

    it("returns false when hasGitRepo is false", async () => {
      model.gitStatus = makeStatus({ has_repo: false });

      const result = await model.syncRepository();

      expect(gitService.sync).not.toHaveBeenCalled();
      expect(result).toBe(false);
    });

    it("returns false when already syncing", async () => {
      model.gitStatus = makeStatus();
      model.syncing = true;

      const result = await model.syncRepository();

      expect(gitService.sync).not.toHaveBeenCalled();
      expect(result).toBe(false);
    });

    it("sets error on API failure", async () => {
      model.gitStatus = makeStatus();
      vi.mocked(gitService.sync).mockRejectedValueOnce(new Error("Sync rejected"));

      const result = await model.syncRepository();

      expect(result).toBe(false);
      expect(model.syncing).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: pull calls gitService.pull
  // ═══════════════════════════════════════════════════════════════
  describe("pullRepository", () => {
    it("calls gitService.pull with correct args", async () => {
      model.gitStatus = makeStatus();
      vi.mocked(gitService.pull).mockResolvedValueOnce({});
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());

      await model.pullRepository();

      expect(gitService.pull).toHaveBeenCalledWith("test-dashboard", "env-123");
      expect(model.pulling).toBe(false);
    });

    it("does nothing when hasGitRepo is false", async () => {
      model.gitStatus = makeStatus({ has_repo: false });

      await model.pullRepository();

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

    it("sets error on API failure", async () => {
      model.gitStatus = makeStatus();
      vi.mocked(gitService.pull).mockRejectedValueOnce(new Error("Pull failed"));

      await model.pullRepository();

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: push calls gitService.push
  // ═══════════════════════════════════════════════════════════════
  describe("pushRepository", () => {
    it("calls gitService.push with correct args", async () => {
      model.gitStatus = makeStatus();
      vi.mocked(gitService.push).mockResolvedValueOnce({});
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());

      await model.pushRepository();

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

    it("does nothing when hasGitRepo is false", async () => {
      model.gitStatus = makeStatus({ has_repo: false });

      await model.pushRepository();

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

    it("sets error on API failure", async () => {
      model.gitStatus = makeStatus();
      vi.mocked(gitService.push).mockRejectedValueOnce(new Error("Push rejected"));

      await model.pushRepository();

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: mutual exclusion — sync/pull/push
  // ═══════════════════════════════════════════════════════════════
  describe("mutual exclusion", () => {
    it("sync/pull/push default to false", () => {
      expect(model.syncing).toBe(false);
      expect(model.pulling).toBe(false);
      expect(model.pushing).toBe(false);
    });

    it("syncing blocks syncRepository", async () => {
      model.gitStatus = makeStatus();
      model.syncing = true;

      const result = await model.syncRepository();

      expect(gitService.sync).not.toHaveBeenCalled();
      expect(result).toBe(false);
    });

    it("pulling blocks pullRepository", async () => {
      model.gitStatus = makeStatus();
      model.pulling = true;

      await model.pullRepository();

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

    it("pushing blocks pushRepository", async () => {
      model.gitStatus = makeStatus();
      model.pushing = true;

      await model.pushRepository();

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loadHistory calls gitService.getHistory
  // ═══════════════════════════════════════════════════════════════
  describe("loadHistory", () => {
    it("calls gitService.getHistory with correct args", async () => {
      const commits = [{ id: "abc123", message: "fix: test" }];
      vi.mocked(gitService.getHistory).mockResolvedValueOnce(commits);

      await model.loadHistory(30);

      expect(gitService.getHistory).toHaveBeenCalledWith("test-dashboard", 30, "env-123");
      expect(model.gitHistory).toEqual(commits);
      expect(model.historyLoading).toBe(false);
    });

    it("sets empty array on API failure", async () => {
      vi.mocked(gitService.getHistory).mockRejectedValueOnce(new Error("Failed"));

      await model.loadHistory();

      expect(model.gitHistory).toEqual([]);
      expect(model.historyLoading).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Derived computations
  // ═══════════════════════════════════════════════════════════════
  describe("derived state", () => {
    it("hasGitRepo is false when gitStatus is null", () => {
      model.gitStatus = null;
      expect(model.hasGitRepo).toBe(false);
    });

    it("hasGitRepo is false when has_repo is false", () => {
      model.gitStatus = makeStatus({ has_repo: false });
      expect(model.hasGitRepo).toBe(false);
    });

    it("hasGitRepo is true when gitStatus has a branch", () => {
      model.gitStatus = makeStatus({ current_branch: "main" });
      expect(model.hasGitRepo).toBe(true);
    });

    it("syncState is NO_REPO when gitStatus is null", () => {
      model.gitStatus = null;
      expect(model.syncState).toBe("NO_REPO");
    });

    it("syncState is SYNCED when repo is clean and synced", () => {
      model.gitStatus = makeStatus({ sync_state: "SYNCED" });
      expect(model.syncState).toBe("SYNCED");
    });

    it("syncState is CHANGES when there are modified files and explicit state is UNKNOWN", () => {
      model.gitStatus = makeStatus({ sync_state: "UNKNOWN", modified_files: ["file1.py"] });
      expect(model.syncState).toBe("CHANGES");
    });

    it("hasChangesToCommit is true when files are changed", () => {
      model.gitStatus = makeStatus({ modified_files: ["file1.py"] });
      expect(model.hasChangesToCommit).toBe(true);
    });

    it("hasChangesToCommit is false when no files changed", () => {
      model.gitStatus = makeStatus();
      expect(model.hasChangesToCommit).toBe(false);
    });

    it("changedChartsCount counts /charts/ paths", () => {
      model.gitStatus = makeStatus({
        modified_files: ["/charts/sales.yaml", "/datasets/orders.yaml"],
      });
      expect(model.changedChartsCount).toBe(1);
      expect(model.changedDatasetsCount).toBe(1);
    });

    it("meta returns correct labels for different sync states", () => {
      model.gitStatus = makeStatus({ sync_state: "SYNCED" });
      expect(model.meta.label).toBe("Synced");

      model.gitStatus = makeStatus({ sync_state: null, modified_files: ["x.py"] });
      expect(model.meta.label).toBe("Modified");

      model.gitStatus = makeStatus({ sync_state: "AHEAD_REMOTE", ahead_count: 3 });
      expect(model.meta.label).toBe("Ahead");

      model.gitStatus = makeStatus({ sync_state: "BEHIND_REMOTE", behind_count: 2 });
      expect(model.meta.label).toBe("Behind");

      model.gitStatus = makeStatus({ sync_state: "DIVERGED", ahead_count: 1, behind_count: 1 });
      expect(model.meta.label).toBe("Diverged");
    });

    it("allChangedFilesList deduplicates across categories", () => {
      model.gitStatus = makeStatus({
        staged_files: ["file1.py", "file2.py"],
        modified_files: ["file2.py", "file3.py"],
        untracked_files: ["file4.py"],
      });
      expect(model.allChangedFilesList).toEqual(["file1.py", "file2.py", "file3.py", "file4.py"]);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleBranchChange
  // ═══════════════════════════════════════════════════════════════
  describe("handleBranchChange", () => {
    it("updates currentBranch and calls loadStatus", () => {
      vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus({ current_branch: "develop" }));

      model.handleBranchChange({ detail: { branch: "develop" } });

      expect(model.currentBranch).toBe("develop");
      expect(gitService.getStatus).toHaveBeenCalledTimes(1);
    });

    it("does nothing when event has no branch", () => {
      model.handleBranchChange({ detail: {} });
      expect(model.currentBranch).toBe("main");
      expect(gitService.getStatus).not.toHaveBeenCalled();
    });
  });
});
// #endregion GitStatusModelTests
