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

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

vi.mock("$lib/api.js", () => ({
  api: {
    getEnvironmentsList: 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: {}, migration: {} })),
}));

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

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

interface EnvironmentFixture {
  id: string;
  name: string;
  stage: string;
  is_production: boolean;
  superset_url: string;
  [key: string]: unknown;
}

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

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

/** Create a mock environment object. */
function makeEnv(id: string, name: string, stage: string, overrides: Partial<EnvironmentFixture> = {}): EnvironmentFixture {
  return {
    id,
    name,
    stage,
    is_production: stage === "PROD",
    superset_url: `https://${name}.example.com`,
    ...overrides,
  };
}

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

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

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: loadEnvironments
  // ═══════════════════════════════════════════════════════════════
  describe("loadEnvironments", () => {
    it("fetches environments via api.getEnvironmentsList", async () => {
      const envs = [makeEnv("env-1", "Dev", "DEV"), makeEnv("env-2", "Prod", "PROD")];
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce(envs);

      await model.loadEnvironments();

      expect(api.getEnvironmentsList).toHaveBeenCalledTimes(1);
      expect(model.environments).toEqual(envs);
      expect(model.loading).toBe(false);
    });

    it("selects first candidate when no preferred stage", async () => {
      const envs = [
        makeEnv("current-env-id", "Current", "DEV"),
        makeEnv("target-1", "Target", "DEV"),
        makeEnv("target-2", "Prod", "PROD"),
      ];
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce(envs);

      await model.loadEnvironments();

      // "current-env-id" is filtered out, first remaining is "target-1"
      expect(model.selectedEnv).toBe("target-1");
    });

    it("selects stage-matched candidate when preferred stage is set", async () => {
      model.preferredTargetStage = "PROD";
      const envs = [
        makeEnv("current-env-id", "Current", "DEV"),
        makeEnv("target-1", "Stage", "PREPROD"),
        makeEnv("target-2", "Prod", "PROD"),
      ];
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce(envs);

      await model.loadEnvironments();

      expect(model.selectedEnv).toBe("target-2");
    });

    it("falls back to first candidate when preferred stage has no match", async () => {
      model.preferredTargetStage = "PROD";
      const envs = [
        makeEnv("current-env-id", "Current", "DEV"),
        makeEnv("target-1", "Stage", "PREPROD"),
      ];
      vi.mocked(api.getEnvironmentsList).mockResolvedValueOnce(envs);

      await model.loadEnvironments();

      expect(model.selectedEnv).toBe("target-1");
    });

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

      await model.loadEnvironments();

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

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

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

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: Derived computations
  // ═══════════════════════════════════════════════════════════════
  describe("derived state", () => {
    it("normalizedPreferredStage is uppercase", () => {
      model.preferredTargetStage = "prod";
      expect(model.normalizedPreferredStage).toBe("PROD");

      model.preferredTargetStage = "Preprod";
      expect(model.normalizedPreferredStage).toBe("PREPROD");

      model.preferredTargetStage = "";
      expect(model.normalizedPreferredStage).toBe("");
    });

    it("deploymentCandidates filters out current envId", () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("env-2", "Stage", "PREPROD"),
      ];

      expect(model.deploymentCandidates).toHaveLength(1);
      expect(model.deploymentCandidates[0].id).toBe("env-2");
    });

    it("deploymentCandidates filters by preferred stage when matched", () => {
      model.preferredTargetStage = "PREPROD";
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("env-2", "Stage", "PREPROD"),
        makeEnv("env-3", "Prod", "PROD"),
      ];

      expect(model.deploymentCandidates).toHaveLength(1);
      expect(model.deploymentCandidates[0].id).toBe("env-2");
    });

    it("deploymentCandidates returns all when no preferred stage", () => {
      model.preferredTargetStage = "";
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("env-2", "Stage", "PREPROD"),
        makeEnv("env-3", "Prod", "PROD"),
      ];

      expect(model.deploymentCandidates).toHaveLength(2);
    });

    it("isProdTarget is true when selected env stage is PROD", () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("env-2", "Prod", "PROD"),
      ];
      model.selectedEnv = "env-2";

      expect(model.isProdTarget).toBe(true);
    });

    it("isProdTarget is false when selected env is not PROD", () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("env-2", "Stage", "PREPROD"),
      ];
      model.selectedEnv = "env-2";

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

    it("normalizeEnvStage returns PROD for is_production environments", () => {
      const env = makeEnv("e1", "Legacy Prod", "DEV", { is_production: true });
      expect(model.normalizeEnvStage(env)).toBe("PROD");
    });

    it("resolveEnvUrl falls back through superset_url, url, empty string", () => {
      expect(model.resolveEnvUrl({ id: "e1", superset_url: "https://superset.example.com" }))
        .toBe("https://superset.example.com");
      expect(model.resolveEnvUrl({ id: "e2", url: "https://fallback.example.com" }))
        .toBe("https://fallback.example.com");
      expect(model.resolveEnvUrl({ id: "e3" }))
        .toBe("");
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // @INVARIANT: handleDeploy
  // ═══════════════════════════════════════════════════════════════
  describe("handleDeploy", () => {
    it("calls gitService.deploy with correct args", async () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("target-1", "Target", "PREPROD"),
      ];
      model.selectedEnv = "target-1";
      vi.mocked(gitService.deploy).mockResolvedValueOnce({ message: "Deployed" });

      await model.handleDeploy();

      expect(gitService.deploy).toHaveBeenCalledWith("test-dashboard", "target-1", "env-1");
      expect(model.deploying).toBe(false);
    });

    it("closes modal on success", async () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("target-1", "Target", "PREPROD"),
      ];
      model.selectedEnv = "target-1";
      model.show = true;
      vi.mocked(gitService.deploy).mockResolvedValueOnce({});

      await model.handleDeploy();

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

    it("does nothing when no environment is selected", async () => {
      model.selectedEnv = "";

      await model.handleDeploy();

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

    it("sets deploying lifecycle", async () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("target-1", "Target", "PREPROD"),
      ];
      model.selectedEnv = "target-1";
      vi.mocked(gitService.deploy).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));

      const promise = model.handleDeploy();
      expect(model.deploying).toBe(true);
      await promise;
      expect(model.deploying).toBe(false);
    });

    it("resets deploying on failure", async () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("target-1", "Target", "PREPROD"),
      ];
      model.selectedEnv = "target-1";
      vi.mocked(gitService.deploy).mockRejectedValueOnce(new Error("Deploy failed"));

      await model.handleDeploy();

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

    it("requires prod confirmation for PROD target", async () => {
      model.envId = "env-1";
      model.environments = [
        makeEnv("env-1", "Current", "DEV"),
        makeEnv("env-2", "Production", "PROD"),
      ];
      model.selectedEnv = "env-2";

      // Mock window.prompt to return wrong slug
      const originalPrompt = globalThis.prompt;
      globalThis.prompt = vi.fn(() => "wrong-slug") as unknown as typeof globalThis.prompt;

      await model.handleDeploy();

      expect(gitService.deploy).not.toHaveBeenCalled();
      expect(model.deploying).toBe(false);

      globalThis.prompt = originalPrompt;
    });
  });
});
// #endregion DeploymentModelTests
