import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';

import { useRecentProjectsSection } from '@features/recent-projects/renderer/hooks/useRecentProjectsSection';
import {
  __resetRecentProjectsClientCacheForTests,
  loadRecentProjectsWithClientCache,
} from '@features/recent-projects/renderer/utils/recentProjectsClientCache';
import {
  invalidateContextScopedRequestEpoch,
  resetContextScopedRequestEpochForTests,
} from '@renderer/store/utils/contextScopedRequestEpoch';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type {
  DashboardRecentProject,
  DashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts';
import type { TeamSummary } from '@shared/types';

(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
  true;

const apiMock = vi.hoisted(() => ({
  getDashboardRecentProjects: vi.fn(),
  teams: {
    aliveList: vi.fn(),
  },
  config: {
    addCustomProjectPath: vi.fn(),
    selectFolders: vi.fn(),
  },
  openPath: vi.fn(),
}));

const storeState = vi.hoisted(() => ({
  globalTasks: [],
  globalTasksInitialized: false,
  globalTasksLoading: false,
  fetchAllTasks: vi.fn(),
  teams: [] as TeamSummary[],
  activeContextId: 'local',
  provisioningRuns: {},
  currentProvisioningRunIdByTeam: {},
  provisioningSnapshotByTeam: {},
  repositoryGroups: [],
  fetchRepositoryGroups: vi.fn(),
  openTeamsTab: vi.fn(),
  fetchSessionsInitial: vi.fn(),
}));

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

vi.mock('@renderer/store', () => {
  const useStore = Object.assign(
    (selector: (state: typeof storeState) => unknown) => selector(storeState),
    {
      getState: () => storeState,
      setState: vi.fn((patch: Partial<typeof storeState>) => {
        Object.assign(storeState, patch);
      }),
    }
  );
  return { useStore };
});

vi.mock('zustand/react/shallow', () => ({
  useShallow: <T,>(selector: T) => selector,
}));

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

function project(id: string, projectPath = `/tmp/${id}`): DashboardRecentProject {
  return {
    id,
    name: id,
    primaryPath: projectPath,
    associatedPaths: [projectPath],
    mostRecentActivity: Date.parse('2026-04-14T12:00:00.000Z'),
    providerIds: ['anthropic'],
    source: 'claude',
    openTarget: {
      type: 'synthetic-path',
      path: projectPath,
    },
  };
}

function payload(id: string, projectPath?: string): DashboardRecentProjectsPayload {
  return {
    projects: [project(id, projectPath)],
    degraded: false,
  };
}

function team(teamName: string, projectPath: string): TeamSummary {
  return {
    teamName,
    displayName: teamName,
    description: '',
    memberCount: 1,
    taskCount: 0,
    lastActivity: null,
    projectPath,
  };
}

async function flushPromises(): Promise<void> {
  await Promise.resolve();
  await Promise.resolve();
}

describe('useRecentProjectsSection', () => {
  let host: HTMLDivElement;
  let root: Root;
  let latest: ReturnType<typeof useRecentProjectsSection> | null;

  function Harness(): React.JSX.Element | null {
    latest = useRecentProjectsSection('', 20);
    return null;
  }

  async function renderHarness(): Promise<void> {
    await act(async () => {
      root.render(React.createElement(Harness));
      await flushPromises();
    });
  }

  beforeEach(() => {
    __resetRecentProjectsClientCacheForTests();
    resetContextScopedRequestEpochForTests();
    vi.clearAllMocks();
    latest = null;
    storeState.globalTasks = [];
    storeState.globalTasksInitialized = false;
    storeState.globalTasksLoading = false;
    storeState.teams = [];
    storeState.activeContextId = 'local';
    storeState.provisioningRuns = {};
    storeState.currentProvisioningRunIdByTeam = {};
    storeState.provisioningSnapshotByTeam = {};
    storeState.repositoryGroups = [];
    apiMock.teams.aliveList.mockResolvedValue([]);

    host = document.createElement('div');
    document.body.appendChild(host);
    root = createRoot(host);
  });

  afterEach(() => {
    act(() => {
      root.unmount();
    });
    host.remove();
    __resetRecentProjectsClientCacheForTests();
    resetContextScopedRequestEpochForTests();
  });

  it('ignores stale recent-project loads after the context epoch changes back to the same id', async () => {
    const oldLocalRequest = deferred<DashboardRecentProjectsPayload>();
    const sshRequest = deferred<DashboardRecentProjectsPayload>();
    const freshLocalRequest = deferred<DashboardRecentProjectsPayload>();

    apiMock.getDashboardRecentProjects
      .mockReturnValueOnce(oldLocalRequest.promise)
      .mockReturnValueOnce(sshRequest.promise)
      .mockReturnValueOnce(freshLocalRequest.promise);

    await renderHarness();
    expect(apiMock.getDashboardRecentProjects).toHaveBeenCalledTimes(1);

    invalidateContextScopedRequestEpoch();
    storeState.activeContextId = 'ssh-dev';
    await renderHarness();
    expect(apiMock.getDashboardRecentProjects).toHaveBeenCalledTimes(2);

    invalidateContextScopedRequestEpoch();
    storeState.activeContextId = 'local';
    await renderHarness();
    expect(apiMock.getDashboardRecentProjects).toHaveBeenCalledTimes(3);

    await act(async () => {
      oldLocalRequest.resolve(payload('old-local'));
      await oldLocalRequest.promise;
      await flushPromises();
    });

    expect(latest?.cards.map((card) => card.name)).toEqual([]);
    expect(latest?.loading).toBe(true);

    await act(async () => {
      freshLocalRequest.resolve(payload('fresh-local'));
      await freshLocalRequest.promise;
      await flushPromises();
    });

    expect(latest?.cards.map((card) => card.name)).toEqual(['fresh-local']);
    expect(latest?.loading).toBe(false);
  });

  it('cancels stale alive-list responses when only the active context changes', async () => {
    const oldAliveRequest = deferred<string[]>();
    const sshAliveRequest = deferred<string[]>();
    const freshAliveRequest = deferred<string[]>();

    await loadRecentProjectsWithClientCache('local', () => Promise.resolve(payload('alpha')), {
      force: true,
    });

    apiMock.getDashboardRecentProjects.mockResolvedValue(payload('alpha'));
    apiMock.teams.aliveList
      .mockReturnValueOnce(oldAliveRequest.promise)
      .mockReturnValueOnce(sshAliveRequest.promise)
      .mockReturnValueOnce(freshAliveRequest.promise);
    storeState.teams = [team('old-team', '/tmp/alpha'), team('fresh-team', '/tmp/alpha')];

    await renderHarness();
    expect(apiMock.teams.aliveList).toHaveBeenCalledTimes(1);

    invalidateContextScopedRequestEpoch();
    storeState.activeContextId = 'ssh-dev';
    await renderHarness();
    expect(apiMock.teams.aliveList).toHaveBeenCalledTimes(2);

    invalidateContextScopedRequestEpoch();
    storeState.activeContextId = 'local';
    await renderHarness();
    expect(apiMock.teams.aliveList).toHaveBeenCalledTimes(3);

    await act(async () => {
      freshAliveRequest.resolve(['fresh-team']);
      await freshAliveRequest.promise;
      await flushPromises();
    });

    expect(latest?.cards[0]?.activeTeams?.map((activeTeam) => activeTeam.teamName)).toEqual([
      'fresh-team',
    ]);

    await act(async () => {
      oldAliveRequest.resolve(['old-team']);
      await oldAliveRequest.promise;
      await flushPromises();
    });

    expect(latest?.cards[0]?.activeTeams?.map((activeTeam) => activeTeam.teamName)).toEqual([
      'fresh-team',
    ]);
  });
});
