import { test, expect, type Page, type Route } from '@playwright/test';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

/**
 * E2E tests for the Tracker dashboard.
 *
 * These tests are deterministic: every call that would hit the backend
 * platform-service is mocked at the Next.js proxy boundary (`/api/tracker/**`,
 * `/api/auth/**`). No live platform-service is required. The only real server
 * handler exercised end-to-end is `/api/health`, whose required env vars are
 * provided by the Playwright `webServer.env` config.
 */

/**
 * Locate the axe-core browser source so we can inject it into the page.
 *
 * axe-core is already present in this pnpm monorepo as a transitive dependency
 * of eslint-plugin-jsx-a11y (pulled in by eslint-config-next), so we resolve it
 * from the store/node_modules rather than declaring a redundant dependency.
 * Playwright runs this suite with cwd at the package root (dashboards/tracker-web).
 */
function resolveAxeSource(): string {
  const candidates = [
    join(process.cwd(), 'node_modules', 'axe-core', 'axe.js'),
    join(process.cwd(), '..', '..', 'node_modules', 'axe-core', 'axe.js'),
  ];
  for (const candidate of candidates) {
    if (existsSync(candidate)) return readFileSync(candidate, 'utf-8');
  }
  // Fall back to scanning the pnpm store at the monorepo root.
  const storeDir = join(process.cwd(), '..', '..', 'node_modules', '.pnpm');
  const entry = readdirSync(storeDir).find(d => d.startsWith('axe-core@'));
  if (!entry) {
    throw new Error('Could not locate axe-core in node_modules or the pnpm store');
  }
  return readFileSync(join(storeDir, entry, 'node_modules', 'axe-core', 'axe.js'), 'utf-8');
}

const AXE_SOURCE = resolveAxeSource();

// ── Fixtures ────────────────────────────────────────────────────────

type RoadmapItem = {
  id: string;
  productId: string;
  type: 'bug' | 'feature' | 'task';
  status: 'open' | 'in_progress' | 'done';
  priority: 'critical' | 'high' | 'medium' | 'low';
  title: string;
  description: string;
  labels: string[];
  assignee: string | null;
  reportedBy: string;
  source: 'internal' | 'user_submitted' | 'auto_detected';
  visibility: 'internal' | 'public';
  voteCount: number;
  commentCount: number;
  targetRelease: string | null;
  createdAt: string;
  updatedAt: string;
};

function makeItem(partial: Partial<RoadmapItem> & { id: string; title: string }): RoadmapItem {
  return {
    productId: 'tracker-e2e',
    type: 'feature',
    status: 'open',
    priority: 'medium',
    description: 'A sample roadmap item used in e2e tests.',
    labels: [],
    assignee: null,
    reportedBy: 'system',
    source: 'internal',
    visibility: 'public',
    voteCount: 0,
    commentCount: 0,
    targetRelease: null,
    createdAt: '2025-01-01T00:00:00.000Z',
    updatedAt: '2025-01-01T00:00:00.000Z',
    ...partial,
  };
}

const ROADMAP_ITEMS: RoadmapItem[] = [
  makeItem({ id: '1', title: 'Dark mode toggle', status: 'open', type: 'feature', voteCount: 12 }),
  makeItem({
    id: '2',
    title: 'Faster sync engine',
    status: 'in_progress',
    type: 'feature',
    priority: 'high',
    voteCount: 8,
  }),
  makeItem({ id: '3', title: 'Export to CSV', status: 'done', type: 'task', voteCount: 5 }),
];

const ROADMAP_STATS = {
  total: 3,
  byStatus: { open: 1, in_progress: 1, done: 1 },
  byType: { feature: 2, task: 1 },
  totalVotes: 25,
};

/** Mock the public roadmap proxy endpoints so the page renders deterministically. */
async function mockRoadmap(page: Page): Promise<void> {
  await page.route('**/api/tracker/**', async (route: Route) => {
    const url = route.request().url();
    if (url.includes('/public/roadmap/stats')) {
      return route.fulfill({ json: ROADMAP_STATS });
    }
    if (url.includes('/public/items/')) {
      const id = url.split('/public/items/')[1]?.split(/[?#]/)[0];
      const item = ROADMAP_ITEMS.find(entry => entry.id === id);
      return item
        ? route.fulfill({ json: item })
        : route.fulfill({ status: 404, json: { error: 'Item not found' } });
    }
    if (url.includes('/public/roadmap')) {
      return route.fulfill({
        json: { items: ROADMAP_ITEMS, total: ROADMAP_ITEMS.length, limit: 100, offset: 0 },
      });
    }
    // Default: empty success so nothing else hangs on the network.
    return route.fulfill({ json: {} });
  });
}

/** Inject axe-core and return only serious/critical accessibility violations. */
async function seriousA11yViolations(page: Page): Promise<{ id: string; impact: string }[]> {
  await page.addScriptTag({ content: AXE_SOURCE });
  const violations = await page.evaluate(async () => {
    // axe is injected onto window by the script tag above.
    const axe = (
      window as unknown as {
        axe: {
          run: (
            ctx: unknown,
            opts: unknown
          ) => Promise<{ violations: { id: string; impact: string | null }[] }>;
        };
      }
    ).axe;
    const result = await axe.run(document, {
      runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] },
    });
    return result.violations.map(v => ({ id: v.id, impact: v.impact ?? 'unknown' }));
  });
  return violations.filter(v => v.impact === 'serious' || v.impact === 'critical');
}

/** Collect uncaught page errors and console.error messages (minus dev-only noise). */
function collectPageErrors(page: Page): string[] {
  const errors: string[] = [];
  const benign = ['favicon', 'Download the React DevTools', 'Failed to load resource', 'net::ERR_'];
  page.on('pageerror', err => errors.push(`pageerror: ${err.message}`));
  page.on('console', msg => {
    if (msg.type() !== 'error') return;
    const text = msg.text();
    if (benign.some(b => text.includes(b))) return;
    errors.push(`console.error: ${text}`);
  });
  return errors;
}

// ── Login page ──────────────────────────────────────────────────────

test.describe('Tracker Login Page', () => {
  test('shows login form with correct branding', async ({ page }) => {
    await page.goto('/login');
    await expect(page.getByRole('heading', { name: 'Tracker' })).toBeVisible();
    await expect(page.getByText('Feature requests, bugs & task management')).toBeVisible();
    await expect(page.getByLabel('Email')).toBeVisible();
    await expect(page.getByLabel('Password')).toBeVisible();
    await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible();
  });

  test('shows credentials hint', async ({ page }) => {
    await page.goto('/login');
    await expect(page.getByText(/platform-service credentials/i)).toBeVisible();
  });

  test('shows error for invalid credentials', async ({ page }) => {
    // Mock the login proxy to reject the credentials deterministically.
    await page.route('**/api/auth/login', (route: Route) =>
      route.fulfill({ status: 401, json: { error: 'Invalid email or password' } })
    );
    await page.goto('/login');
    await page.getByLabel('Email').fill('bad@user.com');
    await page.getByLabel('Password').fill('wrongpassword');
    await page.getByRole('button', { name: /sign in/i }).click();
    await expect(page.getByText('Invalid email or password')).toBeVisible({ timeout: 10000 });
  });

  test('shows loading state on submit', async ({ page }) => {
    // Delay the login response so the loading state stays visible.
    await page.route(
      '**/api/auth/login',
      route =>
        new Promise<void>(resolve =>
          setTimeout(() => {
            resolve();
            route.fulfill({ status: 401, json: { error: 'nope' } });
          }, 3000)
        )
    );
    await page.goto('/login');
    await page.getByLabel('Email').fill('test@example.com');
    await page.getByLabel('Password').fill('password123');
    await page.getByRole('button', { name: /sign in/i }).click();
    await expect(page.getByText('Signing in...')).toBeVisible();
  });

  test('has no serious accessibility violations', async ({ page }) => {
    await page.goto('/login');
    await expect(page.getByRole('heading', { name: 'Tracker' })).toBeVisible();
    const violations = await seriousA11yViolations(page);
    expect(violations, JSON.stringify(violations, null, 2)).toEqual([]);
  });
});

// ── Protected routes ────────────────────────────────────────────────

test.describe('Tracker — Protected Routes', () => {
  test('/ redirects to login when not authenticated', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
  });

  test('/dashboard redirects to login when not authenticated', async ({ page }) => {
    await page.goto('/dashboard');
    await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
  });
});

// ── Login → dashboard happy path (fully mocked) ─────────────────────

test.describe('Tracker — Authenticated dashboard', () => {
  test('login redirects to dashboard and renders stats', async ({ page }) => {
    await page.route('**/api/auth/login', (route: Route) =>
      route.fulfill({
        json: {
          accessToken: 'fake-e2e-token',
          user: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' },
        },
      })
    );
    await page.route('**/api/auth/me', (route: Route) =>
      route.fulfill({
        json: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' },
      })
    );
    await page.route('**/api/tracker/**', (route: Route) => {
      if (route.request().url().includes('/items/stats')) {
        return route.fulfill({
          json: {
            productId: 'tracker-e2e',
            total: 42,
            byType: { bug: 10, feature: 30, task: 2 },
            byStatus: { open: 20, in_progress: 12, done: 10 },
            byPriority: { critical: 2, high: 10, medium: 20, low: 10 },
          },
        });
      }
      return route.fulfill({ json: {} });
    });

    await page.goto('/login');
    await page.getByLabel('Email').fill('admin@example.com');
    await page.getByLabel('Password').fill('correct-password');
    await page.getByRole('button', { name: /sign in/i }).click();

    await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 });
    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await expect(
      page.getByTestId('bl-number-flow').filter({ hasText: '42' }).first()
    ).toBeVisible();
    await expect(page.getByText('admin@example.com')).toBeVisible();
  });

  test('shows a retry path when dashboard stats fail', async ({ page }) => {
    await page.route('**/api/auth/me', (route: Route) =>
      route.fulfill({
        json: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' },
      })
    );
    let allowRecovery = false;
    await page.route('**/api/tracker/**', (route: Route) => {
      if (route.request().url().includes('/items/stats')) {
        return !allowRecovery
          ? route.fulfill({ status: 502, json: { error: 'upstream unavailable' } })
          : route.fulfill({
              json: {
                productId: 'tracker-e2e',
                total: 7,
                byType: { bug: 1, feature: 5, task: 1 },
                byStatus: { open: 4, in_progress: 2, done: 1 },
                byPriority: { critical: 0, high: 2, medium: 4, low: 1 },
              },
            });
      }
      return route.fulfill({ json: {} });
    });

    await page.addInitScript(() => localStorage.setItem('tracker_token', 'fake-e2e-token'));
    await page.goto('/dashboard');
    await expect(page.getByRole('heading', { name: /could not load dashboard/i })).toBeVisible();
    allowRecovery = true;
    await page.getByRole('button', { name: /retry/i }).click();
    await expect(page.getByTestId('bl-number-flow').filter({ hasText: '7' }).first()).toBeVisible();
  });

  test('renders settings for admin configuration', async ({ page }) => {
    await page.route('**/api/auth/me', (route: Route) =>
      route.fulfill({
        json: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' },
      })
    );

    await page.addInitScript(() => localStorage.setItem('tracker_token', 'fake-e2e-token'));
    await page.goto('/dashboard/settings');
    await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Product context' })).toBeVisible();
    await expect(page.getByLabel('Default product ID')).toBeVisible();
    await expect(page.getByText('/api/tracker/[...path]')).toBeVisible();
    await expect(page.getByText('/api/agent/v1/[...path]')).toBeVisible();
    await expect(page.getByText('/api/webhooks/[...path]')).toBeVisible();
    await page.getByLabel('Default product ID').fill('nomgap');
    await page.getByRole('button', { name: /save product context/i }).click();
    await expect(page.getByLabel('Select product')).toHaveValue('nomgap');
    await expect(page.getByText('Saved product context.')).toBeVisible();
    await expect(page.getByRole('link', { name: /open public roadmap/i })).toHaveAttribute(
      'href',
      '/roadmap'
    );
  });
});

// ── Public roadmap (mocked) ─────────────────────────────────────────

test.describe('Tracker — Public Roadmap', () => {
  test.beforeEach(async ({ page }) => {
    await mockRoadmap(page);
  });

  test('renders header and stats from mocked data', async ({ page }) => {
    await page.goto('/roadmap');
    await expect(page.getByRole('heading', { name: /product roadmap/i })).toBeVisible();
    await expect(page.getByText('Total Items')).toBeVisible();
    // totalVotes stat from ROADMAP_STATS
    await expect(page.getByText('25')).toBeVisible();
  });

  test('has search and filter controls', async ({ page }) => {
    await page.goto('/roadmap');
    await expect(page.getByPlaceholder(/search/i).first()).toBeVisible();
  });

  test('has a submit-idea button', async ({ page }) => {
    await page.goto('/roadmap');
    await expect(page.getByRole('button', { name: /submit idea/i })).toBeVisible();
  });

  test('shows status columns in board view with items', async ({ page }) => {
    await page.goto('/roadmap');
    await expect(page.getByRole('heading', { name: 'Planned' })).toBeVisible();
    await expect(page.getByRole('heading', { name: 'In Progress' })).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Complete' })).toBeVisible();
    // Items land in the correct columns
    await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Faster sync engine' })).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Export to CSV' })).toBeVisible();
  });

  test('can toggle between board and list view', async ({ page }) => {
    await page.goto('/roadmap');
    await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
    // The view toggle is a shared SegmentedControl (role=radio), UX-12.1.
    await page.getByRole('radio', { name: 'List', exact: true }).click();
    // List view still shows the items (rendered as rows, not a <table>)
    await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Export to CSV' })).toBeVisible();
  });

  test('opens the submit-idea modal', async ({ page }) => {
    await page.goto('/roadmap');
    await page.getByRole('button', { name: /submit idea/i }).click();
    await expect(page.getByRole('heading', { name: /submit an idea/i })).toBeVisible();
    await expect(page.getByPlaceholder('Your email')).toBeVisible();
  });

  test('prompts for email when voting without a saved email', async ({ page }) => {
    await page.goto('/roadmap');
    await page.getByRole('button', { name: /upvote dark mode toggle/i }).click();
    await expect(page.getByRole('heading', { name: /enter your email to vote/i })).toBeVisible();
  });

  test('links submitted users to a public status page for an item', async ({ page }) => {
    await page.goto('/status/1');
    await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
    await expect(page.getByText('Submission status')).toBeVisible();
    await expect(
      page.getByText('We received this submission and it is waiting for triage.')
    ).toBeVisible();
    await expect(page.getByText('12 votes')).toBeVisible();
    await expect(page.getByRole('link', { name: /back to roadmap/i })).toBeVisible();
  });

  test('shows a helpful message for an unknown public status item', async ({ page }) => {
    await page.goto('/status/missing');
    await expect(page.getByRole('heading', { name: /submission not found/i })).toBeVisible();
    await expect(page.getByRole('link', { name: /view roadmap/i })).toBeVisible();
  });

  test('has no serious accessibility violations', async ({ page }) => {
    await page.goto('/roadmap');
    await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
    const violations = await seriousA11yViolations(page);
    expect(violations, JSON.stringify(violations, null, 2)).toEqual([]);
  });

  test('logs no unexpected console errors', async ({ page }) => {
    const errors = collectPageErrors(page);
    await page.goto('/roadmap');
    await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
    expect(errors, errors.join('\n')).toEqual([]);
  });
});

// ── Health endpoint (real handler, env provided by webServer) ───────

test.describe('Tracker — Health', () => {
  test('GET /api/health returns ok with required env set', async ({ request }) => {
    const res = await request.get('/api/health');
    expect(res.ok()).toBeTruthy();
    const body = await res.json();
    expect(body.status).toBe('ok');
    expect(body.service).toBe('tracker-dashboard');
    expect(Array.isArray(body.checks)).toBe(true);
    expect(body.checks.every((c: { status: string }) => c.status === 'pass')).toBe(true);
  });
});
