import { sql } from "drizzle-orm";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { SnippetRepository } from "../../../database/repositories/snippet-repository.js";

describe("SnippetRepository", () => {
  let adapter: TestSqliteDatabase | null = null;

  afterEach(async () => {
    if (adapter) {
      await adapter.close();
      adapter = null;
    }
  });

  async function createRepository(onWrite?: () => void): Promise<{
    repository: SnippetRepository;
  }> {
    adapter = new TestSqliteDatabase();
    const context = await adapter.connect();
    await adapter.exec(`
      INSERT INTO users (id, username, password_hash) VALUES
        ('user-1', 'user-1', 'hash'),
        ('user-2', 'user-2', 'hash');

      INSERT INTO snippets (
        id, user_id, name, content, description, folder, "order", host_filter
      )
      VALUES
        (1, 'user-1', 'root', 'uptime', NULL, NULL, 2, NULL),
        (2, 'user-1', 'deploy', 'make deploy', 'Deploy app', 'ops', 1, 'linux'),
        (3, 'user-2', 'other', 'whoami', NULL, NULL, 1, NULL);
      INSERT INTO snippet_folders (id, user_id, name, color, icon)
      VALUES
        (1, 'user-1', 'ops', '#123456', 'terminal'),
        (2, 'user-1', 'db', NULL, NULL),
        (3, 'user-2', 'other', NULL, NULL);
    `);

    return {
      repository: new SnippetRepository(context, onWrite),
    };
  }

  it("finds owned snippets only", async () => {
    const { repository } = await createRepository();

    await expect(repository.findOwnedById("user-1", 1)).resolves.toMatchObject({
      id: 1,
      userId: "user-1",
      name: "root",
    });
    await expect(repository.findOwnedById("user-1", 3)).resolves.toBeNull();
    await expect(repository.findOwnedById("user-1", 999)).resolves.toBeNull();
  });

  it("lists folders by name for a user", async () => {
    const { repository } = await createRepository();

    const rows = await repository.listFolders("user-1");

    expect(rows.map((row) => row.name)).toEqual(["db", "ops"]);
  });

  it("lists export data for a user", async () => {
    const { repository } = await createRepository();

    const snippets = await repository.listSnippetsForExport("user-1");
    const folders = await repository.listFoldersForExport("user-1");

    expect(snippets.map((row) => row.name)).toEqual(["root", "deploy"]);
    expect(folders.map((row) => row.name)).toEqual(["db", "ops"]);
  });

  it("lists owned snippets for route merging", async () => {
    const { repository } = await createRepository();

    const rows = await repository.listOwnedSnippets("user-1");

    expect(rows.map((row) => row.name)).toEqual(["root", "deploy"]);
  });

  it("reorders snippets", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    await repository.reorderSnippets("user-1", [
      { id: 1, order: 9, folder: " ops " },
      { id: 999, order: 1 },
    ]);

    await expect(repository.findOwnedById("user-1", 1)).resolves.toMatchObject({
      order: 9,
      folder: "ops",
    });
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("creates snippets with the next folder order", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const created = await repository.createSnippet("user-1", {
      name: " new ",
      content: " echo ok ",
      description: " desc ",
      folder: "ops",
      hostFilter: { os: "linux" },
    });

    expect(created).toMatchObject({
      userId: "user-1",
      name: "new",
      content: "echo ok",
      description: "desc",
      folder: "ops",
      order: 2,
      hostFilter: JSON.stringify({ os: "linux" }),
    });
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("updates snippets and returns the original row for audit names", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const result = await repository.updateSnippet("user-1", 2, {
      name: " deploy new ",
      content: " make deploy2 ",
      description: null,
      folder: null,
      order: 7,
      hostFilter: null,
    });
    const missing = await repository.updateSnippet("user-1", 3, {
      name: "nope",
    });

    expect(result?.existing).toMatchObject({ name: "deploy" });
    expect(result?.updated).toMatchObject({
      name: "deploy new",
      content: "make deploy2",
      description: null,
      folder: null,
      order: 7,
      hostFilter: null,
    });
    expect(missing).toBeNull();
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("deletes snippets and returns the deleted row for audit names", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const deleted = await repository.deleteSnippet("user-1", 2);
    const missing = await repository.deleteSnippet("user-1", 3);

    expect(deleted).toMatchObject({ id: 2, name: "deploy" });
    expect(missing).toBeNull();
    await expect(repository.findOwnedById("user-1", 2)).resolves.toBeNull();
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("deletes all snippets and folders for a user", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    await expect(repository.deleteByUserId("user-1")).resolves.toEqual({
      snippetsDeleted: 2,
      foldersDeleted: 2,
    });

    expect(
      await adapter!.query(sql`SELECT id FROM snippets ORDER BY id`),
    ).toEqual([{ id: 3 }]);
    expect(
      await adapter!.query(sql`SELECT id FROM snippet_folders ORDER BY id`),
    ).toEqual([{ id: 3 }]);
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("bulk imports folders and snippets", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const result = await repository.bulkImport(
      "user-1",
      [
        {
          name: " new snippet ",
          content: " echo hi ",
          description: " desc ",
          folder: " new folder ",
          hostFilter: "linux",
        },
        { name: "", content: "bad" },
        { name: "deploy", content: "skip", folder: "ops" },
      ],
      [
        { name: " new folder ", color: " #fff ", icon: " star " },
        { name: "ops" },
        { name: "" },
      ],
      false,
    );

    expect(result).toEqual({
      snippetsImported: 1,
      snippetsSkipped: 1,
      snippetsUpdated: 0,
      foldersImported: 1,
      foldersSkipped: 1,
      failed: 2,
      errors: [
        "Folder missing name",
        "Snippet 2: name and content are required",
      ],
    });
    await expect(repository.findOwnedById("user-1", 4)).resolves.toMatchObject({
      name: "new snippet",
      content: "echo hi",
      description: "desc",
      folder: "new folder",
      order: 0,
      hostFilter: "linux",
    });
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("bulk import overwrites existing snippets", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const result = await repository.bulkImport(
      "user-1",
      [
        {
          name: "deploy",
          content: "make deploy v2",
          folder: "ops",
          order: 5,
          hostFilter: "prod",
        },
      ],
      undefined,
      true,
    );

    expect(result).toMatchObject({
      snippetsImported: 0,
      snippetsSkipped: 0,
      snippetsUpdated: 1,
      failed: 0,
    });
    await expect(repository.findOwnedById("user-1", 2)).resolves.toMatchObject({
      content: "make deploy v2",
      order: 5,
      hostFilter: "prod",
    });
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("creates folders and rejects duplicate names", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const created = await repository.createFolder(
      "user-1",
      "  new  ",
      " #fff ",
      " star ",
    );
    const duplicate = await repository.createFolder(
      "user-1",
      "ops",
      null,
      null,
    );

    expect(created).toMatchObject({
      userId: "user-1",
      name: "new",
      color: "#fff",
      icon: "star",
    });
    expect(duplicate).toBeNull();
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("updates folder metadata", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    const updated = await repository.updateFolderMetadata(
      "user-1",
      "ops",
      " #abc ",
      undefined,
    );
    const missing = await repository.updateFolderMetadata(
      "user-1",
      "missing",
      null,
      null,
    );

    expect(updated).toMatchObject({
      name: "ops",
      color: "#abc",
      icon: "terminal",
    });
    expect(missing).toBeNull();
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("renames folders and attached snippets", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    await expect(
      repository.renameFolder("user-1", "missing", "new"),
    ).resolves.toEqual({ status: "missing" });
    await expect(
      repository.renameFolder("user-1", "ops", "db"),
    ).resolves.toEqual({
      status: "conflict",
    });
    await expect(
      repository.renameFolder("user-1", "ops", "deploys"),
    ).resolves.toEqual({ status: "renamed" });

    await expect(repository.listFolders("user-1")).resolves.toEqual(
      expect.arrayContaining([expect.objectContaining({ name: "deploys" })]),
    );
    await expect(repository.findOwnedById("user-1", 2)).resolves.toMatchObject({
      folder: "deploys",
    });
    expect(onWrite).toHaveBeenCalledTimes(1);
  });

  it("deletes folders and moves snippets to the root", async () => {
    const onWrite = vi.fn();
    const { repository } = await createRepository(onWrite);

    await repository.deleteFolder("user-1", "ops");

    expect(
      (await repository.listFolders("user-1")).map((row) => row.name),
    ).toEqual(["db"]);
    await expect(repository.findOwnedById("user-1", 2)).resolves.toMatchObject({
      folder: null,
    });
    expect(onWrite).toHaveBeenCalledTimes(1);
  });
});
