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 { taskFixtures } from './page-fixtures';
import { acceptSnapshot, DEFAULT_FRESHNESS_POLICY } from '@/lib/freshness/model';
import { writeSnapshotCache } from '@/lib/freshness/snapshot-cache';
import { validateTaskCollection } from '@/lib/freshness/validators';

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

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

import { TasksPage } from './tasks';

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 };
}

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 renderTasksPage(): Promise<void> {
  const routes: RouteObject[] = [{ path: '/tasks', element: <TasksPage /> }];
  const router = createMemoryRouter(routes, { initialEntries: ['/tasks'] });
  container = document.createElement('div');
  document.body.append(container);
  root = createRoot(container);

  await act(async () => {
    root?.render(<RouterProvider router={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 }));
}

/** Flush pending promise callbacks inside the act environment. */
async function flushAct(): Promise<void> {
  await act(async () => {
    await Promise.resolve();
  });
}

describe('TasksPage', () => {
  it('shows a visible loading state before the tasks request settles', async () => {
    const deferred = createDeferred<typeof taskFixtures>();
    apiMock.mockReturnValueOnce(deferred.promise);

    await renderTasksPage();

    expect(container.textContent).toContain('Loading tasks...');

    await act(async () => {
      deferred.resolve(taskFixtures);
      await deferred.promise;
    });
  });

  it('starts in kanban view, toggles to list view, and opens the read-only modal from cards and rows', async () => {
    apiMock.mockResolvedValueOnce(taskFixtures);

    await renderTasksPage();

    expect(container.textContent).toContain('Not Started');
    expect(container.textContent).toContain('In Progress');
    expect(container.textContent).toContain('Blocked');

    const kanbanCard = [...container.querySelectorAll('button')].find((candidate) =>
      candidate.textContent?.includes('Route /projects/:id'),
    );
    expect(kanbanCard).toBeTruthy();

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

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

    await act(async () => {
      container
        .querySelector('button[aria-label="Close task details"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

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

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

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

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

    expect(container.querySelector('[role="dialog"]')).toBeTruthy();
    expect(container.textContent).toContain('Wire list and kanban modal interactions');
  });

  it('renders a failed fetch as an explicit unavailable state, never an empty healthy board', async () => {
    apiMock.mockRejectedValueOnce(new Error('Tasks request failed'));

    await renderTasksPage();

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

    // Negative controls: no board, no healthy empty-state markers, and the
    // surface is marked unavailable rather than current.
    expect(container.textContent).not.toContain('Not Started');
    expect(container.textContent).not.toContain('No tasks');
    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'unavailable',
    );
  });

  it('recovers to a current board after retrying a failed fetch', async () => {
    apiMock
      .mockRejectedValueOnce(new Error('Tasks request failed'))
      .mockResolvedValueOnce(taskFixtures);

    await renderTasksPage();
    expect(container.querySelector('[role="alert"]')).toBeTruthy();

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

    expect(container.querySelector('[role="alert"]')).toBeNull();
    expect(container.textContent).toContain('Not Started');
    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'current',
    );
  });

  it('labels restored last-known data as stale with source, version, and age until verified', async () => {
    // Seed a last-known snapshot fetched five minutes ago; the page must
    // render it only under an explicit staleness label while the fetch is
    // still in flight.
    const restored = acceptSnapshot({
      value: taskFixtures,
      validate: validateTaskCollection,
      previous: null,
      policy: DEFAULT_FRESHNESS_POLICY,
      source: 'gateway:/api/tasks',
      now: Date.now() - 5 * 60_000,
    });
    if (restored.outcome !== 'accepted') throw new Error('fixture setup failed');
    writeSnapshotCache('tasks', restored.snapshot);

    const deferred = createDeferred<typeof taskFixtures>();
    apiMock.mockReturnValueOnce(deferred.promise);

    await renderTasksPage();

    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'stale',
    );
    const banner = container.querySelector('[role="status"]');
    expect(banner?.textContent).toContain('last-known');
    expect(banner?.textContent).toContain('may be out of date');
    expect(banner?.textContent).toContain('gateway:/api/tasks');
    expect(banner?.textContent).toContain('snapshot v1');
    expect(banner?.textContent).toContain('5m ago');

    // Last-known data still renders as situational awareness under the label.
    expect(container.textContent).toContain('Route /tasks');
    expect(container.textContent).not.toContain('Loading tasks...');

    // Verification lands: the banner clears and the surface becomes current.
    await act(async () => {
      deferred.resolve(taskFixtures);
      await deferred.promise;
    });

    expect(container.querySelector('[role="status"]')).toBeNull();
    expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe(
      'current',
    );
  });
});
