import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { createMemoryRouter, RouterProvider, type RouteObject } from 'react-router-dom';
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { missionFixtures, projectFixtures, taskFixtures } from './page-fixtures';

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

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

import { ProjectDetailPage } from './project-detail';

let root: Root | null = null;
let container: HTMLDivElement;

beforeAll(() => {
  Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
    configurable: true,
    value: true,
  });
});

afterAll(() => {
  Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
});

afterEach(async () => {
  await act(async () => {
    root?.unmount();
  });
  document.body.replaceChildren();
  root = null;
  apiMock.mockReset();
  sessionStorage.clear();
});

async function renderProjectDetailPage(): Promise<ReturnType<typeof createMemoryRouter>> {
  const routes: RouteObject[] = [
    { path: '/projects', element: <p>Projects index target</p> },
    { path: '/projects/:id', element: <ProjectDetailPage /> },
  ];
  const router = createMemoryRouter(routes, { initialEntries: ['/projects/project-1'] });
  container = document.createElement('div');
  document.body.append(container);
  root = createRoot(container);

  await act(async () => {
    root?.render(<RouterProvider router={router} />);
  });

  return router;
}

function clickButtonByText(text: string): void {
  const button = [...container.querySelectorAll('button')].find((candidate) =>
    candidate.textContent?.includes(text),
  );
  if (!button) {
    throw new Error(`Button containing "${text}" not found`);
  }
  button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}

async function flushAct(): Promise<void> {
  await act(async () => {
    await Promise.resolve();
  });
}

interface Deferred<T> {
  promise: Promise<T>;
  resolve: (value: T) => void;
}

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

const projectOneTasks = taskFixtures.filter((task) => task.projectId === 'project-1');

function mockHealthyLoad(): void {
  apiMock
    .mockResolvedValueOnce(projectFixtures[0])
    .mockResolvedValueOnce(missionFixtures)
    .mockResolvedValueOnce(projectOneTasks);
}

describe('ProjectDetailPage', () => {
  it('loads the project, tasks, missions, and optional PRD content for the active project', async () => {
    mockHealthyLoad();

    await renderProjectDetailPage();

    expect(apiMock.mock.calls.map((call) => call[0])).toEqual([
      '/api/projects/project-1',
      '/api/missions',
      '/api/tasks?projectId=project-1',
    ]);

    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'current',
    );
    expect(container.textContent).toContain('Mosaic Stack');
    expect(container.textContent).toContain('Route /projects/:id');
    expect(container.textContent).toContain('Tasks');
    expect(container.textContent).toContain('Done');
    expect(container.textContent).toContain('Blocked');

    await act(async () => {
      clickButtonByText('Missions (1)');
    });

    expect(container.textContent).toContain('Ship web parity');
    expect(container.textContent).not.toContain('Unrelated mission');

    await act(async () => {
      clickButtonByText('PRD');
    });

    expect(container.textContent).toContain('Mosaic Stack PRD');
    expect(container.textContent).toContain('Ship the SPA route parity pages.');
  });

  it('opens and closes the existing read-only task modal from the tasks tab', async () => {
    mockHealthyLoad();

    await renderProjectDetailPage();

    await act(async () => {
      clickButtonByText('Tasks (3)');
    });

    const row = [...container.querySelectorAll('tr')].find((candidate) =>
      candidate.textContent?.includes('Route /projects'),
    );
    expect(row).toBeTruthy();

    await act(async () => {
      row?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    expect(container.querySelector('[role="dialog"]')).toBeTruthy();
    expect(container.textContent).toContain('Read-only modal content should remain intact.');

    const closeButton = container.querySelector('button[aria-label="Close task details"]');
    expect(closeButton).toBeTruthy();

    await act(async () => {
      closeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    expect(container.querySelector('[role="dialog"]')).toBeNull();
  });

  it('shows verified completion verdicts when the task collection is current', async () => {
    mockHealthyLoad();

    await renderProjectDetailPage();

    const doneCard = [...container.querySelectorAll('div')].find(
      (candidate) => candidate.textContent === 'Done1',
    );
    expect(doneCard).toBeTruthy();
    const inProgressCard = [...container.querySelectorAll('div')].find(
      (candidate) => candidate.textContent === 'In Progress1',
    );
    expect(inProgressCard).toBeTruthy();
  });

  it('renders an explicit unavailable missions tab when the missions request fails (partial, not empty)', async () => {
    apiMock
      .mockResolvedValueOnce(projectFixtures[0])
      .mockRejectedValueOnce(new Error('Missions request failed'))
      .mockResolvedValueOnce(projectOneTasks);

    await renderProjectDetailPage();

    // Secondary failure degrades the surface to partial; the project itself
    // still renders.
    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'partial',
    );
    expect(container.textContent).toContain('Mosaic Stack');
    const partial = container.querySelector('[role="status"]');
    expect(partial?.textContent).toContain('Missions');
    expect(partial?.textContent).toContain('unavailable');

    await act(async () => {
      clickButtonByText('Missions (?)');
    });

    const alert = container.querySelector('[role="alert"]');
    expect(alert?.textContent).toContain('Missions request failed');
    // Negative control: a failed fetch must not look like an empty list.
    expect(container.textContent).not.toContain('No missions for this project');
  });

  it('marks derived verdicts unknown when the tasks collection is unavailable', async () => {
    apiMock
      .mockResolvedValueOnce(projectFixtures[0])
      .mockResolvedValueOnce(missionFixtures)
      .mockRejectedValueOnce(new Error('Tasks request failed'));

    await renderProjectDetailPage();

    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'partial',
    );

    // Completion verdicts become unknown ('?') — never green counts.
    for (const label of ['Done', 'In Progress', 'Blocked', 'Tasks']) {
      const unknownCard = [...container.querySelectorAll('div')].find(
        (candidate) => candidate.textContent === `${label}?`,
      );
      expect(unknownCard, `expected ${label} card to render ?`).toBeTruthy();
    }
    // Negative control: no green "Done 1" verdict anywhere.
    expect(
      [...container.querySelectorAll('div')].some((candidate) => candidate.textContent === 'Done1'),
    ).toBe(false);

    await act(async () => {
      clickButtonByText('Tasks (?)');
    });

    const alert = container.querySelector('[role="alert"]');
    expect(alert?.textContent).toContain('Tasks request failed');
    // Negative control: no healthy empty task list from a failed fetch.
    expect(container.textContent).not.toContain('No tasks found');
    expect(container.querySelector('table')).toBeNull();
  });

  it('recovers a partial surface to current after revalidation', async () => {
    apiMock
      .mockResolvedValueOnce(projectFixtures[0])
      .mockResolvedValueOnce(missionFixtures)
      .mockRejectedValueOnce(new Error('Tasks request failed'))
      .mockResolvedValueOnce(projectFixtures[0])
      .mockResolvedValueOnce(missionFixtures)
      .mockResolvedValueOnce(projectOneTasks);

    await renderProjectDetailPage();
    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'partial',
    );

    await act(async () => {
      clickButtonByText('Revalidate');
    });
    await flushAct();

    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'current',
    );
    expect(
      [...container.querySelectorAll('div')].some((candidate) => candidate.textContent === 'Done1'),
    ).toBe(true);
  });

  it("never shows one project's data on another project's route after navigation", async () => {
    mockHealthyLoad();

    const router = await renderProjectDetailPage();
    expect(container.textContent).toContain('Mosaic Stack');

    const deferred = createDeferred<(typeof projectFixtures)[number]>();
    apiMock
      .mockResolvedValueOnce(deferred.promise)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([]);

    await act(async () => {
      await router.navigate('/projects/project-2');
    });

    // While project-2 loads, nothing from project-1 may render on its route.
    expect(container.textContent).toContain('Loading project...');
    expect(container.textContent).not.toContain('Mosaic Stack');
    expect(container.textContent).not.toContain('Route /projects/:id');

    await act(async () => {
      deferred.resolve(projectFixtures[1]!);
      await deferred.promise;
    });

    expect(container.textContent).toContain('Agent Runtime');
    expect(apiMock.mock.calls[3]?.[0]).toBe('/api/projects/project-2');
  });

  it('renders a visible unavailable state when the project request fails and lets the user navigate back', async () => {
    apiMock
      .mockRejectedValueOnce(new Error('Project request failed'))
      .mockResolvedValueOnce(missionFixtures)
      .mockResolvedValueOnce(projectOneTasks);

    const router = await renderProjectDetailPage();

    const alert = container.querySelector('[role="alert"]');
    expect(alert).toBeTruthy();
    expect(alert?.textContent).toContain('Project request failed');
    expect(alert?.textContent).toContain('not an empty result');
    expect(container.textContent).not.toContain('Mosaic Stack');

    await act(async () => {
      clickButtonByText('Back to projects');
    });

    expect(router.state.location.pathname).toBe('/projects');
  });
});
