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

import {
  migrateLocalStorageKey,
  migrateLocalStorageKeys,
} from "./localStorageMigration";

// All tests run inside jsdom (see vite.config.js test.environment). The
// shared localStorage is the real `window.localStorage` jsdom provides;
// we wipe it between tests so one test's writes never leak forward.

beforeEach(() => {
  // Unstub first in case a prior test left `localStorage` stubbed
  // (e.g. the SSR-safe test that sets it to undefined). vitest's
  // `unstubAllGlobals: true` runs AFTER each test's afterEach, which
  // is too late if afterEach itself touches localStorage.
  vi.unstubAllGlobals();
  localStorage.clear();
});

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
  localStorage.clear();
});

describe("migrateLocalStorageKey", () => {
  it("copies the value and removes the old key when only the old key exists", () => {
    localStorage.setItem("magnotia_settings", '{"theme":"dark"}');

    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.getItem("lumotia_settings")).toBe('{"theme":"dark"}');
    expect(localStorage.getItem("magnotia_settings")).toBeNull();
  });

  it("removes the old key and keeps the new key when both exist (lumotia wins)", () => {
    // Contract: if the user has already used the app under the new
    // identifier, the new (lumotia) value is authoritative. The legacy
    // (magnotia) value is deleted to avoid resurrecting it on a future
    // run if the new key were ever cleared.
    localStorage.setItem("magnotia_settings", '{"theme":"light"}');
    localStorage.setItem("lumotia_settings", '{"theme":"dark"}');

    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.getItem("lumotia_settings")).toBe('{"theme":"dark"}');
    expect(localStorage.getItem("magnotia_settings")).toBeNull();
  });

  it("is a no-op when only the new key exists", () => {
    localStorage.setItem("lumotia_settings", '{"theme":"dark"}');

    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.getItem("lumotia_settings")).toBe('{"theme":"dark"}');
    expect(localStorage.getItem("magnotia_settings")).toBeNull();
    expect(localStorage.length).toBe(1);
  });

  it("is a no-op when neither key exists", () => {
    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.length).toBe(0);
  });

  it("is idempotent — second call after a first migration is a no-op", () => {
    localStorage.setItem("magnotia_settings", '{"theme":"dark"}');

    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");
    // Simulate the user editing the new key after the first boot.
    localStorage.setItem("lumotia_settings", '{"theme":"system"}');

    // Second boot: legacy is gone, new has user edits. Migration must
    // not clobber the user edits.
    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.getItem("lumotia_settings")).toBe('{"theme":"system"}');
    expect(localStorage.getItem("magnotia_settings")).toBeNull();
  });

  it("preserves the value's exact bytes — no JSON round-tripping", () => {
    // Some keys hold plain strings ("2026-05-12"), others hold JSON
    // blobs, others hold base64-encoded protobufs. The shim must not
    // parse + reserialise (which would lose insignificant whitespace,
    // alter number precision, or normalise unicode forms). Round-trip
    // a deliberately fragile string and require byte-equality.
    const fragile = '{\n  "n":  1.00000000001,\n  "s":"\\u00e9caf\\u00e9"\n}';
    localStorage.setItem("magnotia_settings", fragile);

    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.getItem("lumotia_settings")).toBe(fragile);
  });

  it("preserves an empty-string value (distinct from null)", () => {
    // Empty string is a valid stored value — distinct from "key
    // absent". The shim must copy "" rather than treating it as a
    // sentinel meaning "nothing to migrate".
    localStorage.setItem("magnotia_settings", "");

    migrateLocalStorageKey("magnotia_settings", "lumotia_settings");

    expect(localStorage.getItem("lumotia_settings")).toBe("");
    expect(localStorage.getItem("magnotia_settings")).toBeNull();
  });

  it("survives a thrown quota / DOMException without re-raising", () => {
    // Quota-exceeded is the canonical failure mode for setItem in
    // private browsing modes and on locked-down sandboxes. The shim
    // is documented as silently swallowing errors to avoid console
    // spam on every window in the multi-window flow.
    localStorage.setItem("magnotia_settings", '{"theme":"dark"}');
    const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
    setItemSpy.mockImplementationOnce(() => {
      throw new DOMException("Quota exceeded", "QuotaExceededError");
    });

    expect(() =>
      migrateLocalStorageKey("magnotia_settings", "lumotia_settings"),
    ).not.toThrow();

    // The error happens on the setItem(new, value) call. The old key
    // is still on disk because removeItem wasn't reached. That's the
    // documented "fall back to the old key on next read" behaviour.
    expect(localStorage.getItem("magnotia_settings")).toBe('{"theme":"dark"}');
    expect(localStorage.getItem("lumotia_settings")).toBeNull();
  });

  it("is a no-op when localStorage is undefined (SSR-safe)", () => {
    // Inside SvelteKit's SSR path localStorage doesn't exist. The shim
    // gates on `typeof localStorage === "undefined"` and must return
    // without throwing. Use vi.stubGlobal so we don't have to redefine
    // jsdom's localStorage property descriptor.
    vi.stubGlobal("localStorage", undefined);

    expect(() =>
      migrateLocalStorageKey("magnotia_settings", "lumotia_settings"),
    ).not.toThrow();
  });
});

describe("migrateLocalStorageKeys", () => {
  it("processes all pairs in input order", () => {
    localStorage.setItem("magnotia_a", "alpha");
    localStorage.setItem("magnotia_b", "bravo");
    localStorage.setItem("magnotia_c", "charlie");

    migrateLocalStorageKeys([
      ["magnotia_a", "lumotia_a"],
      ["magnotia_b", "lumotia_b"],
      ["magnotia_c", "lumotia_c"],
    ]);

    expect(localStorage.getItem("lumotia_a")).toBe("alpha");
    expect(localStorage.getItem("lumotia_b")).toBe("bravo");
    expect(localStorage.getItem("lumotia_c")).toBe("charlie");
    expect(localStorage.getItem("magnotia_a")).toBeNull();
    expect(localStorage.getItem("magnotia_b")).toBeNull();
    expect(localStorage.getItem("magnotia_c")).toBeNull();
  });

  it("a failure on one pair does not prevent the remaining pairs from migrating", () => {
    // Resilience contract: per-key migration must be independent so a
    // quota error on the first key (e.g. a 5 MB legacy blob) doesn't
    // strand the other keys on the old name forever.
    localStorage.setItem("magnotia_a", "alpha");
    localStorage.setItem("magnotia_b", "bravo");
    localStorage.setItem("magnotia_c", "charlie");

    // Capture the real setItem BEFORE spying so the mock can delegate
    // to it for calls that should succeed.
    const realSetItem = Storage.prototype.setItem;
    let throwOnce = true;
    vi.spyOn(Storage.prototype, "setItem").mockImplementation(function (
      this: Storage,
      key: string,
      value: string,
    ) {
      if (throwOnce) {
        throwOnce = false;
        throw new DOMException("Quota exceeded", "QuotaExceededError");
      }
      realSetItem.call(this, key, value);
    });

    expect(() =>
      migrateLocalStorageKeys([
        ["magnotia_a", "lumotia_a"],
        ["magnotia_b", "lumotia_b"],
        ["magnotia_c", "lumotia_c"],
      ]),
    ).not.toThrow();

    // First pair: setItem threw, so the legacy key remains and the new
    // key was never written. The shim caught the DOMException per its
    // documented "silently no-op" contract, leaving the data accessible
    // under the old key on next read.
    expect(localStorage.getItem("lumotia_a")).toBeNull();
    expect(localStorage.getItem("magnotia_a")).toBe("alpha");
    // Subsequent pairs migrated normally — the failure was per-pair,
    // not a hard stop on the batch.
    expect(localStorage.getItem("lumotia_b")).toBe("bravo");
    expect(localStorage.getItem("magnotia_b")).toBeNull();
    expect(localStorage.getItem("lumotia_c")).toBe("charlie");
    expect(localStorage.getItem("magnotia_c")).toBeNull();
  });

  it("handles an empty pairs list cleanly", () => {
    localStorage.setItem("magnotia_settings", '{"theme":"dark"}');

    migrateLocalStorageKeys([]);

    // No migration happened, no error thrown.
    expect(localStorage.getItem("magnotia_settings")).toBe('{"theme":"dark"}');
  });
});
