import { describe, it, expect, vi, beforeEach } from "vitest";

// Hoisted mocks for every Tauri / dataExport dependency. We purposely do
// NOT talk to a real DB or FS — createPreMigrationBackup is a pure
// orchestration layer over `invoke()` + dataExportService helpers.

vi.mock("@tauri-apps/api/core", () => ({
  invoke: vi.fn(),
}));

vi.mock("@tauri-apps/api/app", () => ({
  getVersion: vi.fn(async () => "0.8.3-test"),
}));

vi.mock("./dataExportService", async () => {
  const actual = await vi.importActual<typeof import("./dataExportService")>(
    "./dataExportService",
  );
  return {
    ...actual,
    getExportCategories: vi.fn(async () => []),
    getExportSuppliers: vi.fn(async () => []),
    getExportKeywords: vi.fn(async () => []),
    getExportTransactions: vi.fn(async () => []),
  };
});

import { invoke } from "@tauri-apps/api/core";
import {
  createPreMigrationBackup,
  sanitizeProfileName,
  filesystemSafeIsoTimestamp,
  buildBackupFilename,
  sha256Hex,
  BackupError,
} from "./categoryBackupService";
import type { Profile } from "./profileService";

const mockInvoke = vi.mocked(invoke);

// -----------------------------------------------------------------------------
// Plain-text profile (no PIN).
// -----------------------------------------------------------------------------

const plainProfile: Profile = {
  id: "p1",
  name: "Max",
  color: "#f59e0b",
  pin_hash: null,
  db_filename: "max.db",
  created_at: "2026-01-01T00:00:00Z",
};

const encryptedProfile: Profile = {
  ...plainProfile,
  id: "p2",
  pin_hash: "$argon2id$v=19$m=...",
};

// -----------------------------------------------------------------------------
// Helpers — rebuild the "authoritative" SREF payload the service constructs
// so we can feed read_import_file a consistent round-trip.
// -----------------------------------------------------------------------------

function validEnvelope(): string {
  return JSON.stringify({
    export_type: "transactions_with_categories",
    app_version: "0.8.3-test",
    exported_at: "2026-04-20T00:00:00Z",
    data: {
      categories: [],
      suppliers: [],
      keywords: [],
      transactions: [],
    },
  });
}

function setupHappyPathInvokes(payload: string): void {
  mockInvoke.mockImplementation(async (cmd) => {
    if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
    if (cmd === "write_export_file") return undefined;
    if (cmd === "read_import_file") return payload;
    if (cmd === "get_file_size") return payload.length;
    throw new Error(`unexpected invoke: ${cmd}`);
  });
}

beforeEach(() => {
  mockInvoke.mockReset();
});

// -----------------------------------------------------------------------------
// Pure helper coverage
// -----------------------------------------------------------------------------

describe("sanitizeProfileName", () => {
  it("strips forbidden Windows characters", () => {
    expect(sanitizeProfileName('Bob/\\:*?"<>|Max')).toBe("BobMax");
  });
  it("collapses runs of whitespace into a single dash", () => {
    expect(sanitizeProfileName("Bob    and Alice")).toBe("Bob-and-Alice");
  });
  it("falls back to 'profile' on empty/all-stripped input", () => {
    expect(sanitizeProfileName("")).toBe("profile");
    expect(sanitizeProfileName('   ///\\  ')).toBe("profile");
  });
  it("caps length at 80 characters", () => {
    const long = "A".repeat(200);
    expect(sanitizeProfileName(long).length).toBe(80);
  });
});

describe("filesystemSafeIsoTimestamp", () => {
  it("replaces colons with dashes and drops fractional seconds", () => {
    const ts = filesystemSafeIsoTimestamp(new Date("2026-04-19T14:22:05.456Z"));
    expect(ts).toBe("2026-04-19T14-22-05Z");
  });
});

describe("buildBackupFilename", () => {
  it("concatenates sanitized name + timestamp + .sref suffix", () => {
    const name = buildBackupFilename("Max", new Date("2026-04-19T14:22:05Z"));
    expect(name).toBe("Max_avant-migration-2026-04-19T14-22-05Z.sref");
  });
  it("sanitizes the profile name portion", () => {
    const name = buildBackupFilename("Bob:pro/file", new Date("2026-04-19T14:22:05Z"));
    expect(name.startsWith("Bobprofile_")).toBe(true);
  });
});

describe("sha256Hex", () => {
  it("matches the known SHA-256 of an empty string", async () => {
    const digest = await sha256Hex("");
    expect(digest).toBe(
      "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    );
  });
  it("produces a stable 64-hex-chars digest for a short string", async () => {
    const digest = await sha256Hex("hello");
    expect(digest).toMatch(/^[0-9a-f]{64}$/);
    expect(digest).toBe(
      "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
    );
  });
});

// -----------------------------------------------------------------------------
// createPreMigrationBackup — success + failure modes
// -----------------------------------------------------------------------------

describe("createPreMigrationBackup — happy path (no PIN)", () => {
  it("returns {path, size, checksum, encrypted=false} after round-trip", async () => {
    // The exact payload is constructed inside the service; we hand back the
    // same reference via read_import_file so the checksum matches.
    let captured: string | null = null;
    mockInvoke.mockImplementation(async (cmd, args) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") {
        captured = (args as { content: string }).content;
        return undefined;
      }
      if (cmd === "read_import_file") return captured ?? "";
      if (cmd === "get_file_size") return 4096;
      throw new Error(`unexpected invoke: ${cmd}`);
    });

    const result = await createPreMigrationBackup({ profile: plainProfile });

    expect(result.encrypted).toBe(false);
    expect(result.path.startsWith("/tmp/sr-backups")).toBe(true);
    expect(result.path.endsWith(".sref")).toBe(true);
    expect(result.size).toBe(4096);
    expect(result.checksum).toMatch(/^[0-9a-f]{64}$/);
    expect(result.verifiedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
  });

  it("calls write_export_file with password=null for an unprotected profile", async () => {
    setupHappyPathInvokes(validEnvelope());
    // Override write_export_file so we can inspect args.
    const writeArgs: unknown[] = [];
    mockInvoke.mockImplementation(async (cmd, args) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") {
        writeArgs.push(args);
        return undefined;
      }
      if (cmd === "read_import_file") {
        // Re-serve the content that was just written so the checksum matches.
        const a = writeArgs[0] as { content: string };
        return a.content;
      }
      if (cmd === "get_file_size") return 1;
      throw new Error(`unexpected invoke: ${cmd}`);
    });

    await createPreMigrationBackup({ profile: plainProfile });

    expect(writeArgs).toHaveLength(1);
    expect((writeArgs[0] as { password: string | null }).password).toBeNull();
  });
});

describe("createPreMigrationBackup — encrypted profile (PIN)", () => {
  it("requires a password when the profile has a PIN hash", async () => {
    await expect(
      createPreMigrationBackup({ profile: encryptedProfile }),
    ).rejects.toThrow(BackupError);

    try {
      await createPreMigrationBackup({ profile: encryptedProfile });
    } catch (e) {
      expect(e).toBeInstanceOf(BackupError);
      expect((e as BackupError).code).toBe("missing_password");
    }
  });

  it("forwards the trimmed password to write_export_file AND read_import_file", async () => {
    const writeArgs: Array<{ password: string | null }> = [];
    const readArgs: Array<{ password: string | null }> = [];
    let written = "";
    mockInvoke.mockImplementation(async (cmd, args) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") {
        const a = args as { content: string; password: string | null };
        writeArgs.push({ password: a.password });
        written = a.content;
        return undefined;
      }
      if (cmd === "read_import_file") {
        const a = args as { password: string | null };
        readArgs.push({ password: a.password });
        return written;
      }
      if (cmd === "get_file_size") return written.length;
      throw new Error(`unexpected invoke: ${cmd}`);
    });

    const r = await createPreMigrationBackup({
      profile: encryptedProfile,
      password: "  1234 ",
    });

    expect(r.encrypted).toBe(true);
    expect(writeArgs[0].password).toBe("1234");
    expect(readArgs[0].password).toBe("1234");
  });

  it("rejects when the password is whitespace-only", async () => {
    try {
      await createPreMigrationBackup({
        profile: encryptedProfile,
        password: "   ",
      });
      throw new Error("should have thrown");
    } catch (e) {
      expect(e).toBeInstanceOf(BackupError);
      expect((e as BackupError).code).toBe("missing_password");
    }
  });
});

describe("createPreMigrationBackup — write failures", () => {
  it("raises write_failed when invoke(write_export_file) rejects (generic)", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") throw new Error("write io error");
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect(e).toBeInstanceOf(BackupError);
      expect((e as BackupError).code).toBe("write_failed");
    }
  });

  it("maps 'no space left' to the disk_space code", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") throw new Error("no space left on device");
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("disk_space");
    }
  });

  it("maps 'permission denied' at write time to permission_denied", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") throw new Error("permission denied");
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("permission_denied");
    }
  });
});

describe("createPreMigrationBackup — dir failures", () => {
  it("maps ensure_backup_dir 'create_dir_failed' prefix to the matching code", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") throw new Error("create_dir_failed: EROFS");
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("create_dir_failed");
    }
  });

  it("falls back to documents_dir_unavailable on unknown dir error", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") throw new Error("some weird io error");
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("documents_dir_unavailable");
    }
  });
});

describe("createPreMigrationBackup — integrity check", () => {
  it("raises verification_mismatch when the re-read content has a different checksum", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") return undefined;
      // Deliberately return a DIFFERENT, but still valid, envelope so the
      // parse check passes but the SHA-256 differs.
      if (cmd === "read_import_file")
        return JSON.stringify({
          export_type: "transactions_with_categories",
          app_version: "0.0.0",
          exported_at: "2025-01-01T00:00:00Z",
          data: { categories: [{ id: 99 }] },
        });
      if (cmd === "get_file_size") return 123;
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("verification_mismatch");
      expect((e as BackupError).detail).toContain("checksum_diff");
    }
  });

  it("raises verification_mismatch when the re-read content is not JSON", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") return undefined;
      if (cmd === "read_import_file") return "not{json";
      if (cmd === "get_file_size") return 42;
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("verification_mismatch");
      expect((e as BackupError).detail).toMatch(/envelope_parse/);
    }
  });

  it("raises verification_mismatch when the envelope is the wrong export_type", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") return undefined;
      if (cmd === "read_import_file")
        return JSON.stringify({
          export_type: "categories_only",
          app_version: "0.0.0",
          exported_at: "2025-01-01T00:00:00Z",
          data: { categories: [] },
        });
      if (cmd === "get_file_size") return 42;
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("verification_mismatch");
      expect((e as BackupError).detail).toContain("envelope_type");
    }
  });

  it("raises read_back_failed when invoke(read_import_file) rejects", async () => {
    mockInvoke.mockImplementation(async (cmd) => {
      if (cmd === "ensure_backup_dir") return "/tmp/sr-backups";
      if (cmd === "write_export_file") return undefined;
      if (cmd === "read_import_file") throw new Error("file gone");
      throw new Error(`unexpected invoke: ${cmd}`);
    });
    try {
      await createPreMigrationBackup({ profile: plainProfile });
      throw new Error("should have thrown");
    } catch (e) {
      expect((e as BackupError).code).toBe("read_back_failed");
    }
  });
});
