import { afterEach, beforeEach, describe, test, expect } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { ATTACHMENTS } from "@/core/constants.js";
import {
  buildVideoAttachmentSlug,
  ensureUniqueVideoSlug,
  sanitizeJiraAttachmentFilename,
  selectJiraIssueImageAttachments,
  selectJiraIssueVideoAttachments,
} from "@/utils/jira-attachments.js";
import type { JiraAttachment } from "@/types/index.js";

function makeAttachment(overrides: Partial<JiraAttachment> = {}): JiraAttachment {
  return {
    id: "1",
    filename: "image.png",
    mimeType: "image/png",
    size: 100,
    content: "https://example.com/image.png",
    thumbnail: null,
    created: "2026-01-01T00:00:00Z",
    ...overrides,
  };
}

describe("sanitizeJiraAttachmentFilename", () => {
  test("passes through safe characters unchanged", () => {
    expect(sanitizeJiraAttachmentFilename("hello-world.png")).toBe("hello-world.png");
  });

  test("replaces spaces with underscores", () => {
    expect(sanitizeJiraAttachmentFilename("file name.jpg")).toBe("file_name.jpg");
  });

  test("replaces forward slashes", () => {
    expect(sanitizeJiraAttachmentFilename("path/to/file.png")).toBe("path_to_file.png");
  });

  test("replaces non-ASCII characters", () => {
    expect(sanitizeJiraAttachmentFilename("über.png")).toBe("_ber.png");
  });

  test("empty string returns empty string", () => {
    expect(sanitizeJiraAttachmentFilename("")).toBe("");
  });

  test("preserves dots and hyphens", () => {
    expect(sanitizeJiraAttachmentFilename("my-file.v2.tar.gz")).toBe("my-file.v2.tar.gz");
  });
});

describe("selectJiraIssueImageAttachments", () => {
  test("returns empty array when both inputs are empty", () => {
    expect(selectJiraIssueImageAttachments([], [], 5)).toEqual([]);
  });

  test("filters out non-image mimeTypes", () => {
    const pdf = makeAttachment({ mimeType: "application/pdf" });
    const img = makeAttachment({ mimeType: "image/jpeg" });
    const result = selectJiraIssueImageAttachments([pdf, img], [], 5);
    expect(result).toHaveLength(1);
    expect(result[0]!.mimeType).toBe("image/jpeg");
  });

  test("filters out attachments exceeding MAX_SIZE_BYTES", () => {
    const big = makeAttachment({ size: ATTACHMENTS.MAX_SIZE_BYTES + 1 });
    const ok = makeAttachment({ size: ATTACHMENTS.MAX_SIZE_BYTES });
    const result = selectJiraIssueImageAttachments([big, ok], [], 5);
    expect(result).toHaveLength(1);
    expect(result[0]!.size).toBe(ATTACHMENTS.MAX_SIZE_BYTES);
  });

  test("caps result at maxImages", () => {
    const imgs = Array.from({ length: 10 }, (_, i) =>
      makeAttachment({ id: String(i), filename: `img${i}.png` }),
    );
    const result = selectJiraIssueImageAttachments(imgs, [], 3);
    expect(result).toHaveLength(3);
  });

  test("issue attachments come before parent attachments", () => {
    const issue = makeAttachment({ id: "issue", filename: "issue.png" });
    const parent = makeAttachment({ id: "parent", filename: "parent.png" });
    const result = selectJiraIssueImageAttachments([issue], [parent], 5);
    expect(result[0]!.id).toBe("issue");
    expect(result[1]!.id).toBe("parent");
  });

  test("cap is applied across merged issue + parent arrays", () => {
    const issue = [makeAttachment({ id: "i1" }), makeAttachment({ id: "i2" })];
    const parent = [makeAttachment({ id: "p1" }), makeAttachment({ id: "p2" })];
    const result = selectJiraIssueImageAttachments(issue, parent, 3);
    expect(result).toHaveLength(3);
    expect(result.map((r) => r.id)).toEqual(["i1", "i2", "p1"]);
  });
});

function makeVideo(overrides: Partial<JiraAttachment> = {}): JiraAttachment {
  return makeAttachment({
    filename: "screen-recording.mp4",
    mimeType: "video/mp4",
    size: 5_000_000,
    content: "https://example.com/video.mp4",
    ...overrides,
  });
}

describe("selectJiraIssueVideoAttachments", () => {
  test("returns empty array when both inputs are empty", () => {
    expect(selectJiraIssueVideoAttachments([], [], 1)).toEqual([]);
  });

  test("filters out non-video mimeTypes", () => {
    const png = makeVideo({ mimeType: "image/png" });
    const mp4 = makeVideo({ mimeType: "video/mp4" });
    const result = selectJiraIssueVideoAttachments([png, mp4], [], 5);
    expect(result).toHaveLength(1);
    expect(result[0]!.mimeType).toBe("video/mp4");
  });

  test("accepts every supported mime type", () => {
    const types = ATTACHMENTS.VIDEO.MIME_TYPES;
    const inputs = types.map((m, i) => makeVideo({ id: String(i), mimeType: m }));
    const result = selectJiraIssueVideoAttachments(inputs, [], inputs.length);
    expect(result).toHaveLength(types.length);
  });

  test("filters out videos exceeding VIDEO.MAX_SIZE_BYTES", () => {
    const big = makeVideo({ size: ATTACHMENTS.VIDEO.MAX_SIZE_BYTES + 1 });
    const ok = makeVideo({ size: ATTACHMENTS.VIDEO.MAX_SIZE_BYTES });
    const result = selectJiraIssueVideoAttachments([big, ok], [], 5);
    expect(result).toHaveLength(1);
    expect(result[0]!.size).toBe(ATTACHMENTS.VIDEO.MAX_SIZE_BYTES);
  });

  test("respects 5MB image cap independently — selector uses VIDEO cap, not the image one", () => {
    // Many bug videos are >5MB; the image-only MAX_SIZE_BYTES (5MB) must NOT apply.
    const tenMb = makeVideo({ size: 10 * 1024 * 1024 });
    const result = selectJiraIssueVideoAttachments([tenMb], [], 5);
    expect(result).toHaveLength(1);
  });

  test("caps result at maxVideos", () => {
    const vids = Array.from({ length: 4 }, (_, i) =>
      makeVideo({ id: String(i), filename: `v${i}.mp4` }),
    );
    const result = selectJiraIssueVideoAttachments(vids, [], 1);
    expect(result).toHaveLength(1);
  });

  test("issue videos come before parent videos", () => {
    const issue = makeVideo({ id: "issue", filename: "issue.mp4" });
    const parent = makeVideo({ id: "parent", filename: "parent.mp4" });
    const result = selectJiraIssueVideoAttachments([issue], [parent], 5);
    expect(result.map((r) => r.id)).toEqual(["issue", "parent"]);
  });
});

describe("buildVideoAttachmentSlug", () => {
  test("strips extension and sanitizes", () => {
    expect(buildVideoAttachmentSlug("screen recording.mp4")).toBe("screen_recording");
  });

  test("preserves dots inside the stem", () => {
    expect(buildVideoAttachmentSlug("bug.repro.2025.mov")).toBe("bug.repro.2025");
  });

  test("sanitizes special chars in the stem", () => {
    expect(buildVideoAttachmentSlug("@@@.webm")).toBe("___");
  });

  test("falls back to 'video' when the stem sanitizes to empty", () => {
    expect(buildVideoAttachmentSlug("")).toBe("video");
  });

  test("filename without extension still produces a slug", () => {
    expect(buildVideoAttachmentSlug("noext")).toBe("noext");
  });
});

describe("ensureUniqueVideoSlug", () => {
  let baseDir: string;

  beforeEach(() => {
    baseDir = mkdtempSync(join(tmpdir(), "ensure-unique-slug-"));
  });

  afterEach(() => {
    rmSync(baseDir, { recursive: true, force: true });
  });

  test("returns the original slug when no directory exists", () => {
    expect(ensureUniqueVideoSlug(baseDir, "demo")).toBe("demo");
  });

  test("returns slug_2 when the original slug directory exists", () => {
    mkdirSync(join(baseDir, "demo"));
    expect(ensureUniqueVideoSlug(baseDir, "demo")).toBe("demo_2");
  });

  test("returns slug_3 when slug and slug_2 both exist", () => {
    mkdirSync(join(baseDir, "demo"));
    mkdirSync(join(baseDir, "demo_2"));
    expect(ensureUniqueVideoSlug(baseDir, "demo")).toBe("demo_3");
  });

  test("skips already-taken intermediate suffixes", () => {
    mkdirSync(join(baseDir, "demo"));
    mkdirSync(join(baseDir, "demo_2"));
    mkdirSync(join(baseDir, "demo_3"));
    mkdirSync(join(baseDir, "demo_4"));
    expect(ensureUniqueVideoSlug(baseDir, "demo")).toBe("demo_5");
  });

  test("returns slug_<timestamp> as fallback when all numeric suffixes are taken", () => {
    mkdirSync(join(baseDir, "demo"));
    for (let i = 2; i < 100; i++) mkdirSync(join(baseDir, `demo_${i}`));
    const result = ensureUniqueVideoSlug(baseDir, "demo");
    expect(result.startsWith("demo_")).toBe(true);
    // The fallback uses Date.now(), which is a 13-digit ms epoch in 2025–2026.
    const suffix = result.slice("demo_".length);
    expect(/^\d{10,}$/.test(suffix)).toBe(true);
  });
});
