import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { create } from 'zustand';

import { createProjectSlice } from '../../../src/renderer/store/slices/projectSlice';
import { createRepositorySlice } from '../../../src/renderer/store/slices/repositorySlice';
import {
  invalidateContextScopedRequestEpoch,
  resetContextScopedRequestEpochForTests,
} from '../../../src/renderer/store/utils/contextScopedRequestEpoch';

import type { AppState } from '../../../src/renderer/store/types';
import type { Project, RepositoryGroup } from '../../../src/renderer/types/data';

const apiMock = vi.hoisted(() => ({
  getProjects: vi.fn(),
  getRepositoryGroups: vi.fn(),
}));

vi.mock('@renderer/api', () => ({
  api: apiMock,
}));

function deferred<T>(): {
  promise: Promise<T>;
  resolve: (value: T) => void;
} {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((innerResolve) => {
    resolve = innerResolve;
  });
  return { promise, resolve };
}

function project(id: string, path = `/${id}`): Project {
  return {
    id,
    path,
    name: id,
    sessions: [],
    totalSessions: 0,
    createdAt: 0,
    mostRecentSession: 0,
  };
}

function repositoryGroup(id: string, path = `/${id}`): RepositoryGroup {
  return {
    id,
    identity: null,
    name: id,
    totalSessions: 0,
    mostRecentSession: 0,
    worktrees: [
      {
        id: `${id}-worktree`,
        path,
        name: id,
        isMainWorktree: true,
        source: 'unknown',
        sessions: [],
        totalSessions: 0,
        createdAt: 0,
        mostRecentSession: 0,
      },
    ],
  };
}

function createProjectRepositoryStore() {
  return create<AppState>()((set, get, store) =>
    ({
      ...createProjectSlice(set as never, get as never, store as never),
      ...createRepositorySlice(set as never, get as never, store as never),
      activeContextId: 'local',
      activeProjectId: null,
      fetchSessionsInitial: vi.fn(async () => undefined),
    }) as unknown as AppState
  );
}

describe('project and repository context races', () => {
  beforeEach(() => {
    resetContextScopedRequestEpochForTests();
    apiMock.getProjects.mockReset();
    apiMock.getRepositoryGroups.mockReset();
  });

  afterEach(() => {
    resetContextScopedRequestEpochForTests();
    vi.restoreAllMocks();
  });

  it('applies current-context project loads', async () => {
    const store = createProjectRepositoryStore();
    apiMock.getProjects.mockResolvedValue([project('current-project')]);

    await store.getState().fetchProjects();

    expect(store.getState().projects).toEqual([project('current-project')]);
    expect(store.getState().projectsInitialized).toBe(true);
    expect(store.getState().projectsLoading).toBe(false);
  });

  it('ignores project loads resolved for a previous context', async () => {
    const store = createProjectRepositoryStore();
    const localProjects = deferred<Project[]>();
    const currentProjects = [project('ssh-project', '/ssh/project')];
    apiMock.getProjects.mockReturnValueOnce(localProjects.promise);

    const fetchPromise = store.getState().fetchProjects();
    expect(store.getState().projectsLoading).toBe(true);

    store.setState({
      activeContextId: 'ssh-dev',
      projects: currentProjects,
      projectsLoading: false,
      projectsInitialized: true,
    });
    localProjects.resolve([project('local-project', '/local/project')]);
    await fetchPromise;

    expect(store.getState().projects).toBe(currentProjects);
    expect(store.getState().projectsLoading).toBe(false);
  });

  it('ignores project loads resolved before a same-context epoch reset', async () => {
    const store = createProjectRepositoryStore();
    const oldLocalProjects = deferred<Project[]>();
    const currentProjects = [project('fresh-local-project', '/fresh-local/project')];
    apiMock.getProjects.mockReturnValueOnce(oldLocalProjects.promise);

    const fetchPromise = store.getState().fetchProjects();
    expect(store.getState().projectsLoading).toBe(true);

    invalidateContextScopedRequestEpoch();
    store.setState({
      activeContextId: 'local',
      projects: currentProjects,
      projectsLoading: false,
      projectsInitialized: true,
    });
    oldLocalProjects.resolve([project('old-local-project', '/old-local/project')]);
    await fetchPromise;

    expect(store.getState().projects).toBe(currentProjects);
    expect(store.getState().projectsLoading).toBe(false);
  });

  it('applies current-context repository group loads', async () => {
    const store = createProjectRepositoryStore();
    apiMock.getRepositoryGroups.mockResolvedValue([repositoryGroup('current-repo')]);

    await store.getState().fetchRepositoryGroups();

    expect(store.getState().repositoryGroups).toEqual([repositoryGroup('current-repo')]);
    expect(store.getState().repositoryGroupsInitialized).toBe(true);
    expect(store.getState().repositoryGroupsLoading).toBe(false);
  });

  it('ignores repository group loads resolved for a previous context', async () => {
    const store = createProjectRepositoryStore();
    const localGroups = deferred<RepositoryGroup[]>();
    const currentGroups = [repositoryGroup('ssh-repo', '/ssh/repo')];
    apiMock.getRepositoryGroups.mockReturnValueOnce(localGroups.promise);

    const fetchPromise = store.getState().fetchRepositoryGroups();
    expect(store.getState().repositoryGroupsLoading).toBe(true);

    store.setState({
      activeContextId: 'ssh-dev',
      repositoryGroups: currentGroups,
      repositoryGroupsLoading: false,
      repositoryGroupsInitialized: true,
    });
    localGroups.resolve([repositoryGroup('local-repo', '/local/repo')]);
    await fetchPromise;

    expect(store.getState().repositoryGroups).toBe(currentGroups);
    expect(store.getState().repositoryGroupsLoading).toBe(false);
  });

  it('ignores repository group loads resolved before a same-context epoch reset', async () => {
    const store = createProjectRepositoryStore();
    const oldLocalGroups = deferred<RepositoryGroup[]>();
    const currentGroups = [repositoryGroup('fresh-local-repo', '/fresh-local/repo')];
    apiMock.getRepositoryGroups.mockReturnValueOnce(oldLocalGroups.promise);

    const fetchPromise = store.getState().fetchRepositoryGroups();
    expect(store.getState().repositoryGroupsLoading).toBe(true);

    invalidateContextScopedRequestEpoch();
    store.setState({
      activeContextId: 'local',
      repositoryGroups: currentGroups,
      repositoryGroupsLoading: false,
      repositoryGroupsInitialized: true,
    });
    oldLocalGroups.resolve([repositoryGroup('old-local-repo', '/old-local/repo')]);
    await fetchPromise;

    expect(store.getState().repositoryGroups).toBe(currentGroups);
    expect(store.getState().repositoryGroupsLoading).toBe(false);
  });
});
