import { expect } from "@esm-bundle/chai";
import sinon from "sinon";
import { Persistence } from "../../src/core/Persistence.js";

describe("Core: Persistence", () => {
  let persistence;
  let mockDB;
  let mockStore;
  let mockTransaction;
  let mockRequest;
  let globalObj;

  beforeEach(() => {
    persistence = new Persistence();

    // Mock IndexedDB
    mockStore = {
      put: sinon.stub(),
      get: sinon.stub(),
      delete: sinon.stub(),
    };

    mockTransaction = {
      objectStore: sinon.stub().returns(mockStore),
    };

    mockDB = {
      objectStoreNames: {
        contains: sinon.stub().returns(false),
      },
      createObjectStore: sinon.stub(),
      transaction: sinon.stub().returns(mockTransaction),
    };

    // Use window or self for browser environment
    globalObj =
      typeof window !== "undefined"
        ? window
        : typeof self !== "undefined"
        ? self
        : globalThis;
  });

  const createMockRequest = () => {
    // Mock indexedDB.open - create a new request each time
    mockRequest = {
      onerror: null,
      onsuccess: null,
      onupgradeneeded: null,
      result: mockDB,
    };

    // Mock indexedDB using defineProperty since it's read-only
    Object.defineProperty(globalObj, "indexedDB", {
      value: {
        open: sinon.stub().returns(mockRequest),
      },
      writable: true,
      configurable: true,
    });

    return mockRequest;
  };

  it("CoA 1: init should create database and object stores", async () => {
    const request = createMockRequest();

    // Start init, which will call indexedDB.open
    const initPromise = persistence.init();

    // Trigger upgrade first (happens synchronously during open)
    if (request.onupgradeneeded) {
      request.onupgradeneeded({ target: { result: mockDB } });
    }

    // Then trigger success
    if (request.onsuccess) {
      request.onsuccess({ target: { result: mockDB } });
    }

    await initPromise;

    expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 6)).to.be.true;
    expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be
      .true;
    expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to
      .be.true;
    expect(mockDB.createObjectStore.calledWith("Campaign", { keyPath: "id" }))
      .to.be.true;
    expect(persistence.db).to.equal(mockDB);
  });

  it("CoA 2: saveRun should store run data with active_run id", async () => {
    persistence.db = mockDB;
    const runData = { seed: 12345, depth: 5, squad: [] };

    const mockPutRequest = {
      onsuccess: null,
      onerror: null,
    };
    mockStore.put.returns(mockPutRequest);

    const savePromise = persistence.saveRun(runData);
    mockPutRequest.onsuccess();

    await savePromise;

    expect(mockDB.transaction.calledWith(["Runs"], "readwrite")).to.be.true;
    expect(mockStore.put.calledOnce).to.be.true;
    const savedData = mockStore.put.firstCall.args[0];
    expect(savedData.id).to.equal("active_run");
    expect(savedData.seed).to.equal(12345);
  });

  it("CoA 3: loadRun should retrieve active_run data", async () => {
    persistence.db = mockDB;
    const savedData = { id: "active_run", seed: 12345, depth: 5 };

    const mockGetRequest = {
      onsuccess: null,
      onerror: null,
      result: savedData,
    };
    mockStore.get.returns(mockGetRequest);

    const loadPromise = persistence.loadRun();
    mockGetRequest.onsuccess();

    const result = await loadPromise;

    expect(mockDB.transaction.calledWith(["Runs"], "readonly")).to.be.true;
    expect(mockStore.get.calledWith("active_run")).to.be.true;
    expect(result).to.deep.equal(savedData);
  });

  it("CoA 4: clearRun should delete active_run", async () => {
    persistence.db = mockDB;

    const mockDeleteRequest = {
      onsuccess: null,
      onerror: null,
    };
    mockStore.delete.returns(mockDeleteRequest);

    const deletePromise = persistence.clearRun();
    mockDeleteRequest.onsuccess();

    await deletePromise;

    expect(mockDB.transaction.calledWith(["Runs"], "readwrite")).to.be.true;
    expect(mockStore.delete.calledWith("active_run")).to.be.true;
  });

  it("CoA 5: saveRoster should wrap roster data with id", async () => {
    persistence.db = mockDB;
    const rosterData = { roster: [], graveyard: [] };

    const mockPutRequest = {
      onsuccess: null,
      onerror: null,
    };
    mockStore.put.returns(mockPutRequest);

    const savePromise = persistence.saveRoster(rosterData);
    mockPutRequest.onsuccess();

    await savePromise;

    expect(mockDB.transaction.calledWith(["Roster"], "readwrite")).to.be.true;
    expect(mockStore.put.calledOnce).to.be.true;
    const savedData = mockStore.put.firstCall.args[0];
    expect(savedData.id).to.equal("player_roster");
    expect(savedData.data).to.deep.equal(rosterData);
  });

  it("CoA 6: loadRoster should extract data from stored object", async () => {
    persistence.db = mockDB;
    const storedData = {
      id: "player_roster",
      data: { roster: [], graveyard: [] },
    };

    const mockGetRequest = {
      onsuccess: null,
      onerror: null,
      result: storedData,
    };
    mockStore.get.returns(mockGetRequest);

    const loadPromise = persistence.loadRoster();
    mockGetRequest.onsuccess();

    const result = await loadPromise;

    expect(mockDB.transaction.calledWith(["Roster"], "readonly")).to.be.true;
    expect(mockStore.get.calledWith("player_roster")).to.be.true;
    expect(result).to.deep.equal(storedData.data);
  });

  it("CoA 7: loadRoster should return null if no data exists", async () => {
    persistence.db = mockDB;

    const mockGetRequest = {
      onsuccess: null,
      onerror: null,
      result: undefined,
    };
    mockStore.get.returns(mockGetRequest);

    const loadPromise = persistence.loadRoster();
    mockGetRequest.onsuccess();

    const result = await loadPromise;

    expect(result).to.be.null;
  });

  it("CoA 8: saveRun should auto-init if db not initialized", async () => {
    const request = createMockRequest();

    const runData = { seed: 12345 };
    const mockPutRequest = {
      onsuccess: sinon.stub(),
      onerror: null,
    };
    mockStore.put.returns(mockPutRequest);

    // Start saveRun, which will trigger init
    const savePromise = persistence.saveRun(runData);

    // Trigger upgrade and success for init
    if (request.onupgradeneeded) {
      request.onupgradeneeded({ target: { result: mockDB } });
    }
    if (request.onsuccess) {
      request.onsuccess({ target: { result: mockDB } });
    }

    // Wait a bit for init to complete, then trigger put success
    await new Promise((resolve) => setTimeout(resolve, 10));
    mockPutRequest.onsuccess();

    await savePromise;

    expect(persistence.db).to.equal(mockDB);
    expect(mockStore.put.calledOnce).to.be.true;
  });

  it("CoA 9: saveCampaign should store campaign data with campaign_data id", async () => {
    persistence.db = mockDB;
    const campaignData = {
      completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"],
    };

    const mockPutRequest = {
      onsuccess: null,
      onerror: null,
    };
    mockStore.put.returns(mockPutRequest);

    const savePromise = persistence.saveCampaign(campaignData);
    mockPutRequest.onsuccess();

    await savePromise;

    expect(mockDB.transaction.calledWith(["Campaign"], "readwrite")).to.be.true;
    expect(mockStore.put.calledOnce).to.be.true;
    const savedData = mockStore.put.firstCall.args[0];
    expect(savedData.id).to.equal("campaign_data");
    expect(savedData.data).to.deep.equal(campaignData);
  });

  it("CoA 10: loadCampaign should extract data from stored object", async () => {
    persistence.db = mockDB;
    const storedData = {
      id: "campaign_data",
      data: {
        completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"],
      },
    };

    const mockGetRequest = {
      onsuccess: null,
      onerror: null,
      result: storedData,
    };
    mockStore.get.returns(mockGetRequest);

    const loadPromise = persistence.loadCampaign();
    mockGetRequest.onsuccess();

    const result = await loadPromise;

    expect(mockDB.transaction.calledWith(["Campaign"], "readonly")).to.be.true;
    expect(mockStore.get.calledWith("campaign_data")).to.be.true;
    expect(result).to.deep.equal(storedData.data);
  });

  it("CoA 11: loadCampaign should return null if no data exists", async () => {
    persistence.db = mockDB;

    const mockGetRequest = {
      onsuccess: null,
      onerror: null,
      result: undefined,
    };
    mockStore.get.returns(mockGetRequest);

    const loadPromise = persistence.loadCampaign();
    mockGetRequest.onsuccess();

    const result = await loadPromise;

    expect(result).to.be.null;
  });
});
