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

// ── Mocks ────────────────────────────────────────────────────────
vi.mock("../../../services/gitService.js", () => ({
  gitService: {
    getBranches: vi.fn(),
    checkoutBranch: vi.fn(),
    createBranch: 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 { BranchModel } from "../BranchModel.svelte.ts";
import { gitService } from "../../../services/gitService.js";

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

interface BranchFixture {
  name: string;
  [key: string]: unknown;
}

interface CheckoutError {
  message: string;
  detail?: { message?: string; next_steps?: string[] };
}

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

/** Create a model with default values. */
function createModel(overrides: Partial<{ dashboardId: string; envId: string | null; currentBranch: string; onChange: ((event: { branch: string }) => void) | null }> = {}): BranchModel {
  return new BranchModel({
    dashboardId: "test-dashboard",
    envId: "env-123",
    currentBranch: "main",
    onChange: null,
    ...overrides,
  });
}

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

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

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loadBranches
  // ═══════════════════════════════════════════════════════════════
  describe("loadBranches", () => {
    it("fetches branches via gitService.getBranches", async () => {
      const branches: BranchFixture[] = [{ name: "main" }, { name: "develop" }];
      vi.mocked(gitService.getBranches).mockResolvedValueOnce(branches);

      await model.loadBranches("test-dashboard", "env-123");

      expect(gitService.getBranches).toHaveBeenCalledWith("test-dashboard", "env-123");
      expect(model.branches).toEqual(branches);
      expect(model.loading).toBe(false);
    });

    it("uses default dashboardId when not passed", async () => {
      vi.mocked(gitService.getBranches).mockResolvedValueOnce([]);

      await model.loadBranches();

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

    it("sets loading lifecycle", async () => {
      vi.mocked(gitService.getBranches).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));

      const promise = model.loadBranches("test-dashboard", "env-123");
      expect(model.loading).toBe(true);
      await promise;
      expect(model.loading).toBe(false);
    });

    it("sets error on API failure", async () => {
      vi.mocked(gitService.getBranches).mockRejectedValueOnce(new Error("Failed to load"));

      await model.loadBranches("test-dashboard", "env-123");

      expect(model.branches).toEqual([]);
      expect(model.loading).toBe(false);
    });

    it("sets empty branches and error on failure", async () => {
      vi.mocked(gitService.getBranches).mockRejectedValueOnce(new Error("Network error"));

      await model.loadBranches("test-dashboard", "env-123");

      expect(model.branches).toEqual([]);
      expect(model.loading).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleCheckout
  // ═══════════════════════════════════════════════════════════════
  describe("handleCheckout", () => {
    it("calls gitService.checkoutBranch with correct args", async () => {
      vi.mocked(gitService.checkoutBranch).mockResolvedValueOnce({});

      await model.handleCheckout("test-dashboard", "develop", "env-123");

      expect(gitService.checkoutBranch).toHaveBeenCalledWith("test-dashboard", "develop", "env-123");
      expect(model.currentBranch).toBe("develop");
      expect(model.checkingOut).toBe(false);
    });

    it("does nothing when branchName is empty", async () => {
      await model.handleCheckout("test-dashboard", "", "env-123");

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

    it("fires onChange callback on success", async () => {
      const onChange = vi.fn();
      model.onChange = onChange;
      vi.mocked(gitService.checkoutBranch).mockResolvedValueOnce({});

      await model.handleCheckout("test-dashboard", "feature", "env-123");

      expect(onChange).toHaveBeenCalledWith({ branch: "feature" });
    });

    it("sets branchError on failure", async () => {
      const error: CheckoutError = {
        message: "Checkout failed",
        detail: { message: "Uncommitted changes", next_steps: ["Stash or commit"] },
      };
      vi.mocked(gitService.checkoutBranch).mockRejectedValueOnce(error);

      await model.handleCheckout("test-dashboard", "develop", "env-123");

      expect(model.branchError).not.toBeNull();
      expect(model.branchError!.message).toBe("Uncommitted changes");
      expect(model.branchError!.next_steps).toContain("Stash or commit");
      expect(model.checkingOut).toBe(false);
    });

    it("uses default dashboardId and envId when not passed", async () => {
      vi.mocked(gitService.checkoutBranch).mockResolvedValueOnce({});

      await model.handleCheckout(undefined, "develop", undefined);

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleCreate
  // ═══════════════════════════════════════════════════════════════
  describe("handleCreate", () => {
    it("calls gitService.createBranch with correct args", async () => {
      vi.mocked(gitService.createBranch).mockResolvedValueOnce({});
      vi.mocked(gitService.getBranches).mockResolvedValueOnce([]);

      await model.handleCreate("test-dashboard", "feature-x", "main", "env-123");

      expect(gitService.createBranch).toHaveBeenCalledWith(
        "test-dashboard",
        "feature-x",
        "main",
        "env-123",
      );
      expect(model.creating).toBe(false);
    });

    it("reloads branches on success", async () => {
      const updatedBranches: BranchFixture[] = [{ name: "main" }, { name: "feature-x" }];
      vi.mocked(gitService.createBranch).mockResolvedValueOnce({});
      vi.mocked(gitService.getBranches).mockResolvedValueOnce(updatedBranches);

      await model.handleCreate("test-dashboard", "feature-x", "main", "env-123");

      expect(gitService.getBranches).toHaveBeenCalledWith("test-dashboard", "env-123");
      expect(model.branches).toEqual(updatedBranches);
    });

    it("resets create form on success", async () => {
      model.showCreate = true;
      model.newBranchName = "feature-x";
      vi.mocked(gitService.createBranch).mockResolvedValueOnce({});
      vi.mocked(gitService.getBranches).mockResolvedValueOnce([]);

      await model.handleCreate("test-dashboard", "feature-x", "main", "env-123");

      expect(model.showCreate).toBe(false);
      expect(model.newBranchName).toBe("");
    });

    it("does nothing when name is empty", async () => {
      await model.handleCreate("test-dashboard", "", "main", "env-123");

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

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

      await model.handleCreate("test-dashboard", "feature-x", "main", "env-123");

      expect(model.branchError).not.toBeNull();
      expect(model.branchError!.message).toBe("Create failed");
      expect(model.creating).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: toggleCreateForm
  // ═══════════════════════════════════════════════════════════════
  describe("toggleCreateForm", () => {
    it("toggles showCreate from false to true", () => {
      model.showCreate = false;
      model.toggleCreateForm();
      expect(model.showCreate).toBe(true);
    });

    it("toggles showCreate from true to false and clears newBranchName", () => {
      model.showCreate = true;
      model.newBranchName = "wip";
      model.toggleCreateForm();
      expect(model.showCreate).toBe(false);
      expect(model.newBranchName).toBe("");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleSelect
  // ═══════════════════════════════════════════════════════════════
  describe("handleSelect", () => {
    it("calls handleCheckout with branch name from event", async () => {
      vi.mocked(gitService.checkoutBranch).mockResolvedValueOnce({});

      await model.handleSelect({ target: { value: "develop" } });

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

    it("does nothing when event has no value", async () => {
      await model.handleSelect({ target: { value: "" } });

      expect(gitService.checkoutBranch).not.toHaveBeenCalled();
    });
  });
});
// #endregion BranchModelTests
