import * as SecureStore from "expo-secure-store";
import {
  clearOfflineCredential,
  saveOfflineCredential,
  verifyOfflineCredential,
} from "../../src/services/offlineCredentialStore";

const getItem = SecureStore.getItemAsync as jest.Mock;
const setItem = SecureStore.setItemAsync as jest.Mock;
const deleteItem = SecureStore.deleteItemAsync as jest.Mock;

const KEY = "cc_offline_credential";
// Sel deterministe : getRandomBytesAsync est mocke sur [1..16] (jest.setup).
const SALT = "0102030405060708090a0b0c0d0e0f10";
const user = { id: 7, email: "tech@x.com", name: "Tech", role: "technicien" } as never;

// Mirroir du mock argon2 (jest.setup) : encodedHash deterministe.
const enc = (salt: string, pw: string) => `$argon2id$mock$${salt}$${pw}`;

function buildRecord(over: Record<string, unknown> = {}) {
  return JSON.stringify({
    baseUrl: "https://api.test",
    email: "tech@x.com",
    salt: SALT,
    encodedHash: enc(SALT, "pw"),
    user,
    savedAt: 1_000_000,
    ...over,
  });
}

describe("offlineCredentialStore", () => {
  beforeEach(() => {
    getItem.mockReset();
    setItem.mockReset();
    deleteItem.mockReset();
    jest.spyOn(Date, "now").mockReturnValue(1_000_000 + 1000); // peu apres savedAt
  });

  afterEach(() => {
    (Date.now as jest.Mock).mockRestore?.();
  });

  describe("saveOfflineCredential", () => {
    it("persiste email minuscule + salt + encodedHash argon2 + user en SecureStore", async () => {
      await saveOfflineCredential("https://api.test", "Tech@X.com", "pw", user);
      expect(setItem).toHaveBeenCalledTimes(1);
      const [key, raw] = setItem.mock.calls[0];
      expect(key).toBe(KEY);
      const rec = JSON.parse(raw);
      expect(rec.email).toBe("tech@x.com"); // normalise
      expect(rec.salt).toBe(SALT);
      expect(rec.encodedHash).toBe(enc(SALT, "pw"));
      expect(rec.user).toEqual(user);
      expect(typeof rec.savedAt).toBe("number");
      // jamais le mot de passe en clair
      expect(raw).not.toContain('"pw"');
    });
  });

  describe("verifyOfflineCredential", () => {
    it("retourne l'utilisateur quand email + mot de passe concordent", async () => {
      getItem.mockResolvedValue(buildRecord());
      const result = await verifyOfflineCredential("https://api.test", "tech@x.com", "pw");
      expect(result).toEqual(user);
    });

    it("email insensible a la casse / espaces", async () => {
      getItem.mockResolvedValue(buildRecord());
      const result = await verifyOfflineCredential("https://api.test", "  TECH@x.com ", "pw");
      expect(result).toEqual(user);
    });

    it("null si pas de credential en cache", async () => {
      getItem.mockResolvedValue(null);
      expect(await verifyOfflineCredential("https://api.test", "tech@x.com", "pw")).toBeNull();
    });

    it("null si mauvais mot de passe (hash different)", async () => {
      getItem.mockResolvedValue(buildRecord());
      expect(await verifyOfflineCredential("https://api.test", "tech@x.com", "WRONG")).toBeNull();
    });

    it("null si instance (baseUrl) differente", async () => {
      getItem.mockResolvedValue(buildRecord());
      expect(await verifyOfflineCredential("https://autre.test", "tech@x.com", "pw")).toBeNull();
    });

    it("null si email different", async () => {
      getItem.mockResolvedValue(buildRecord());
      expect(await verifyOfflineCredential("https://api.test", "autre@x.com", "pw")).toBeNull();
    });

    it("null si credential expire (> 30 jours)", async () => {
      getItem.mockResolvedValue(buildRecord({ savedAt: 1_000_000 }));
      (Date.now as jest.Mock).mockReturnValue(1_000_000 + 31 * 24 * 60 * 60 * 1000);
      expect(await verifyOfflineCredential("https://api.test", "tech@x.com", "pw")).toBeNull();
    });

    it("null si JSON corrompu", async () => {
      getItem.mockResolvedValue("{not-json");
      expect(await verifyOfflineCredential("https://api.test", "tech@x.com", "pw")).toBeNull();
    });
  });

  describe("clearOfflineCredential", () => {
    it("supprime la cle SecureStore", async () => {
      await clearOfflineCredential();
      expect(deleteItem).toHaveBeenCalledWith(KEY);
    });
  });
});
