import React from 'react';
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { ToastProvider } from './toast';
import { I18nProvider } from './i18n/I18nProvider';
import ProfilePage from './views/ProfilePage';
import CareerProfilePage from './views/CareerProfilePage';
import { api } from './api';

jest.mock('./api', () => ({
  api: {
    get: jest.fn(),
    post: jest.fn(),
    put: jest.fn(),
    patch: jest.fn(),
    delete: jest.fn(),
    interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
  },
}));

jest.mock('./components/GoogleAuthCard', () => () => null);
jest.mock('./components/CropImageDialog', () => () => null);

const mockedApi = api as jest.Mocked<typeof api>;

let extractionRunsResponse: any[] = [];

const structuredCv = {
  version: '1',
  metadata: {
    profileVersion: 3,
    appliedExtractionRunId: 12,
    updatedAtUtc: '2026-03-28T12:00:00Z',
    fields: {
      'contact.fullName': { confidence: 0.92, method: 'llm', sourceBlockId: 'block-1', reviewState: 'suggested', sourceSnippet: 'Demo User' },
      summary: { confidence: 0.71, method: 'deterministic', sourceBlockId: 'block-2', reviewState: 'suggested', sourceSnippet: 'Built backend systems' },
      skills: { confidence: 0.68, method: 'deterministic', sourceBlockId: 'block-3', reviewState: 'suggested', sourceSnippet: '.NET' },
    },
  },
  contact: {
    fullName: 'Demo User',
    headline: 'Backend Developer',
    email: 'demo@example.com',
  },
  summary: ['Built backend systems'],
  jobs: [
    {
      title: 'System Developer',
      company: 'Demo Co',
      location: 'Oslo',
      start: '2020',
      end: '2024',
      isCurrent: false,
      bullets: ['Built backend systems'],
      skills: ['.NET', 'SQL'],
    },
  ],
  education: [],
  skills: ['.NET', 'SQL'],
  languages: [{ name: 'English', level: 'Native' }],
  interests: ['Cooking'],
  otherSections: [],
  sections: [
    { name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 },
    { name: 'Skills', content: '.NET\nSQL', wordCount: 2 },
  ],
};

function renderWith(Component: React.ComponentType) {
  return render(
    <ToastProvider>
      <I18nProvider>
        <Component />
      </I18nProvider>
    </ToastProvider>,
  );
}

// These exercise the master-CV editing surface, which lives on /career (CareerProfilePage) after
// the Phase 2.2 split. ProfilePage no longer carries it.
function renderPage(section: 'profile' | 'import' = 'profile') {
  window.history.replaceState({}, '', `/career?section=${section}`);
  return renderWith(CareerProfilePage);
}
void ProfilePage;

beforeEach(() => {
  extractionRunsResponse = [{
    id: 12,
    trigger: 'upload',
    status: 'applied',
    artifactFileName: 'resume.pdf',
    startedAtUtc: '2026-03-28T12:00:00Z',
    completedAtUtc: '2026-03-28T12:00:05Z',
    appliedAtUtc: '2026-03-28T12:00:05Z',
    parserVersion: 'm005-s01',
    normalizerVersion: 'm005-s01',
    llmPromptVersion: 'm005-s01',
  }];
  mockedApi.get.mockImplementation((url: string) => {
    if (url === '/career/profile') {
      // Phase 3: /career reads the structured profile from the relational source of truth.
      return Promise.resolve({
        data: {
          profile: structuredCv,
          cvText: 'Professional Summary\nBuilt backend systems',
          completeness: { percent: 70, missing: ['Projects'], sections: [] },
        },
      } as any);
    }
    if (url === '/career/profile/versions') {
      return Promise.resolve({ data: [] } as any);
    }
    if (url === '/auth/me') {
      return Promise.resolve({
        data: {
          provider: 'local',
          email: 'demo@example.com',
          userName: 'demo',
          firstName: 'Demo',
          lastName: 'User',
          displayName: 'Demo User',
          profileCvText: 'Professional Summary\nBuilt backend systems',
          profileCvStructureJson: JSON.stringify(structuredCv),
          googleLink: { linked: false },
        },
      } as any);
    }
    if (url === '/profile-cv/runs') {
      return Promise.resolve({ data: extractionRunsResponse } as any);
    }
    if (/^\/profile-cv\/runs\/\d+\/diff$/.test(url)) {
      return Promise.resolve({ data: {
        runId: 13,
        status: 'pending_review',
        diff: { totalAdded: 3, totalUpdated: 1, totalLowConfidence: 1, hasChanges: true, categories: [
          { category: 'Skills', added: [{ id: 'Skills|docker', label: 'Docker', confidence: 'High' }], updated: [], unchangedCount: 2, lowConfidenceCount: 0 },
          { category: 'Languages', added: [{ id: 'Languages|french', label: 'French', confidence: 'Low' }], updated: [], unchangedCount: 1, lowConfidenceCount: 1 },
          { category: 'Experience', added: [{ id: 'Experience|new co engineer', label: 'Engineer - New Co', confidence: 'High' }], updated: [{ id: 'Experience|demo co developer', label: 'Developer - Demo Co', confidence: 'High' }], unchangedCount: 0, lowConfidenceCount: 0 },
        ] },
      } } as any);
    }
    if (url === '/jobapplications') {
      return Promise.resolve({
        data: {
          items: [
            {
              id: 42,
              jobTitle: 'Senior Backend Engineer',
              company: { id: 7, name: 'Acme Systems' },
              status: 'Waiting',
              dateApplied: '2026-03-20',
              daysSince: 10,
              description: 'Build API integrations and platform workflows.',
              responseReceived: false,
            },
          ],
          total: 1,
          page: 1,
          pageSize: 100,
        },
      } as any);
    }
    return Promise.resolve({ data: {} } as any);
  });
  mockedApi.post.mockImplementation((url: string, payload?: any, config?: any) => {
    if (url === '/profile-cv/parse') {
      return Promise.resolve({
        data: {
          structuredCv: {
            ...structuredCv,
            sections: [
              { name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 },
              { name: 'Core Skills', content: '.NET\nSQL\nAzure', wordCount: 3 },
            ],
            skills: ['.NET', 'SQL', 'Azure'],
          },
        },
      } as any);
    }
    if (url === '/profile-cv/reprocess') {
      return Promise.resolve({ data: { reprocessed: true } } as any);
    }
    return Promise.resolve({ data: {} } as any);
  });
  mockedApi.put.mockResolvedValue({ data: {} } as any);
  window.localStorage.clear();
  window.history.replaceState({}, '', '/career');
});

afterEach(() => {
  jest.clearAllMocks();
});

test('profile page loads persisted structured cv and can re-parse it', async () => {
  renderPage();

  expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
  expect(screen.getByText(/profile sections/i)).toBeInTheDocument();
  expect(screen.getAllByText(/career information/i).length).toBeGreaterThan(0);
  expect(screen.getByLabelText(/full name/i)).toHaveValue('Demo User');
  expect(screen.getByText(/high 92%/i)).toBeInTheDocument();
  expect(screen.getByText(/block-1/i)).toBeInTheDocument();

  fireEvent.click(screen.getByRole('tab', { name: /import review/i }));
  expect(screen.getByText(/extraction history/i)).toBeInTheDocument();
  expect(screen.getByText(/resume.pdf/i)).toBeInTheDocument();
  expect(screen.getByText(/current run/i)).toBeInTheDocument();
  expect(screen.queryByText(/template-driven cv builder/i)).not.toBeInTheDocument();
  expect(screen.getAllByText(/original import/i).length).toBeGreaterThan(0);
  const originalExtractionToggle = screen.getByRole('button', { name: /original import/i });
  expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false');
  expect(screen.getAllByText(/professional summary/i).length).toBeGreaterThan(0);

  fireEvent.click(originalExtractionToggle);
  expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'true');
  expect(await screen.findByLabelText(/profile cv \/ master resume text/i)).toHaveValue('Professional Summary\nBuilt backend systems');

  // Structure overview is hidden from the default workflow (Phase 1 increment 2); reveal it.
  fireEvent.click(screen.getByRole('button', { name: /advanced cv tools/i }));

  const analyzeButton = screen.getByRole('button', { name: /read sections/i });
  await waitFor(() => expect(analyzeButton).toBeEnabled());
  fireEvent.click(analyzeButton);

  await waitFor(() => {
    expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/parse', { text: 'Professional Summary\nBuilt backend systems' });
  });

  expect(screen.getAllByText(/core skills/i).length).toBeGreaterThan(0);
});

test('saving the career profile PUTs the structured profile and cv text unchanged (Phase 1 refactor invariant)', async () => {
  renderPage();

  const saveButton = await screen.findByRole('button', { name: /save changes/i });
  await waitFor(() => expect(saveButton).toBeEnabled());
  fireEvent.click(saveButton);

  await waitFor(() => {
    expect(mockedApi.put).toHaveBeenCalledWith(
      '/career/profile',
      expect.objectContaining({
        profile: expect.objectContaining({ contact: expect.objectContaining({ fullName: 'Demo User' }) }),
        cvText: 'Professional Summary\nBuilt backend systems',
      }),
    );
  });
});

test('editing a field in an extracted section updates parent state and flows into save (Phase 1 increment 2)', async () => {
  renderPage();

  // Renders existing data through the extracted PersonalInformationSection.
  const nameField = await screen.findByLabelText(/full name/i);
  expect(nameField).toHaveValue('Demo User');

  // Editing the child field updates the parent's structuredCv (controlled input reflects it back).
  fireEvent.change(nameField, { target: { value: 'Edited Name' } });
  expect(nameField).toHaveValue('Edited Name');

  const saveButton = screen.getByRole('button', { name: /save changes/i });
  await waitFor(() => expect(saveButton).toBeEnabled());
  fireEvent.click(saveButton);

  // The edited value reaches the unchanged save path.
  await waitFor(() => {
    expect(mockedApi.put).toHaveBeenCalledWith(
      '/career/profile',
      expect.objectContaining({
        profile: expect.objectContaining({ contact: expect.objectContaining({ fullName: 'Edited Name' }) }),
        cvText: expect.any(String),
      }),
    );
  });
});

test('profile page can reprocess from stored artifact history', async () => {
  renderPage('import');

  expect(await screen.findByText(/extraction history/i)).toBeInTheDocument();
  const reprocessButton = screen.getByRole('button', { name: /reprocess cv/i });
  fireEvent.click(reprocessButton);

  await waitFor(() => {
    expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/reprocess');
  });
});

test('active extraction polling refreshes run status without resetting unsaved career fields', async () => {
  extractionRunsResponse = [{
    id: 15,
    trigger: 'upload',
    status: 'queued',
    artifactFileName: 'active-cv.pdf',
    startedAtUtc: '2026-03-28T12:00:00Z',
    parserVersion: 'm005-s01',
    normalizerVersion: 'm005-s01',
    llmPromptVersion: 'm005-s01',
    operation: {
      id: '00000000-0000-0000-0000-000000000015',
      taskType: 'cv.process',
      status: 'queued',
      createdAtUtc: '2026-03-28T12:00:00Z',
      canCancel: true,
      canRetry: false,
    },
  }];
  let poll: (() => void) | undefined;
  const interval = jest.spyOn(window, 'setInterval').mockImplementation((handler: TimerHandler, timeout?: number) => {
    if (timeout === 4000 && typeof handler === 'function') poll = handler as () => void;
    return 1 as any;
  });

  renderPage();
  const nameField = await screen.findByLabelText(/full name/i);
  await waitFor(() => expect(poll).toBeDefined());
  fireEvent.change(nameField, { target: { value: 'Unsaved Poll-Safe Name' } });

  extractionRunsResponse = [{ ...extractionRunsResponse[0], status: 'running', operation: { ...extractionRunsResponse[0].operation, status: 'running' } }];
  await act(async () => {
    poll?.();
    await Promise.resolve();
    await Promise.resolve();
  });

  await waitFor(() => expect(mockedApi.get.mock.calls.filter(([url]) => url === '/profile-cv/runs').length).toBeGreaterThanOrEqual(2));
  expect(mockedApi.get.mock.calls.filter(([url]) => url === '/career/profile')).toHaveLength(1);
  expect(nameField).toHaveValue('Unsaved Poll-Safe Name');
  expect(screen.getByText('Unsaved changes')).toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
  await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/career/profile', expect.objectContaining({
    profile: expect.objectContaining({ contact: expect.objectContaining({ fullName: 'Unsaved Poll-Safe Name' }) }),
  })));
  interval.mockRestore();
});

test('profile page shows durable CV operation state and retries a failed run', async () => {
  extractionRunsResponse = [{
    id: 14,
    trigger: 'upload',
    status: 'queued',
    artifactFileName: 'synthetic-cv.md',
    startedAtUtc: '2026-03-28T12:00:00Z',
    parserVersion: 'm005-s01',
    normalizerVersion: 'm005-s01',
    llmPromptVersion: 'm005-s01',
    operation: {
      id: '00000000-0000-0000-0000-000000000014',
      taskType: 'cv.process',
      status: 'failed',
      subjectType: 'cv_extraction_run',
      createdAtUtc: '2026-03-28T12:00:00Z',
      failureCategory: 'provider_unavailable',
      canCancel: false,
      canRetry: true,
    },
  }];
  renderPage('import');

  expect(await screen.findByText('failed')).toBeInTheDocument();
  fireEvent.click(screen.getByRole('button', { name: /retry processing/i }));

  await waitFor(() => {
    expect(mockedApi.post).toHaveBeenCalledWith('/operations/00000000-0000-0000-0000-000000000014/retry');
  });
});

test('profile page keeps raw extraction collapsed until expanded', async () => {
  renderPage('import');

  expect(await screen.findByText(/your career information stays front and center/i)).toBeInTheDocument();

  const originalExtractionToggle = screen.getByRole('button', { name: /original import/i });
  expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false');
  expect(screen.queryByRole('button', { name: /copy cv text/i })).not.toBeInTheDocument();

  fireEvent.click(originalExtractionToggle);

  expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'true');
  expect(await screen.findByLabelText(/profile cv \/ master resume text/i)).toHaveValue('Professional Summary\nBuilt backend systems');
  expect(screen.getByRole('button', { name: /copy cv text/i })).toBeEnabled();
});

test('saving the master profile (career) persists the structured profile via /career/profile', async () => {
  // Phase 3: /career saves the master profile through the relational API. The payload carries the
  // structured profile object (not the legacy flat blob) and never identity fields.
  mockedApi.put.mockResolvedValue({ data: { profile: structuredCv, completeness: { percent: 70, missing: [], sections: [] }, cvText: '' } } as any);
  renderPage();

  expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
  const fullNameInput = screen.getByLabelText(/full name/i);
  fireEvent.change(fullNameInput, { target: { value: 'Updated Demo User' } });

  const saveButton = screen.getByRole('button', { name: /save changes/i });
  await waitFor(() => expect(saveButton).toBeEnabled());
  fireEvent.click(saveButton);

  await waitFor(() => {
    expect(mockedApi.put).toHaveBeenCalledWith('/career/profile', expect.anything());
  });

  const payload = mockedApi.put.mock.calls[0][1] as any;
  expect(payload.profile.contact.fullName).toBe('Updated Demo User');
  expect(payload.profile.skills).toEqual(['.NET', 'SQL']);
  expect(payload.profile.jobs[0].title).toBe('System Developer');
  // The career save must NOT carry identity fields (they belong to /profile).
  expect(payload.email).toBeUndefined();
  expect(payload.displayName).toBeUndefined();
});

test('/career shows the profile completeness overview', async () => {
  renderWith(CareerProfilePage);
  expect(await screen.findByText(/profile completeness/i)).toBeInTheDocument();
  expect(await screen.findByText(/70%/)).toBeInTheDocument();
});

test('/career localizes completeness and review confidence without changing CV data', async () => {
  window.localStorage.setItem('uiLanguage', 'nb');
  renderWith(CareerProfilePage);

  expect(await screen.findByText(/Legg til neste: Prosjekter/i)).toBeInTheDocument();
  fireEvent.click(screen.getByRole('tab', { name: 'Karriereprofil' }));
  expect(screen.getByText(/Høy 92%/i)).toBeInTheDocument();
  expect(screen.getByDisplayValue('Demo User')).toBeInTheDocument();
});

test('/career separates overview, profile and import while preserving unsaved profile edits', async () => {
  renderWith(CareerProfilePage);
  await screen.findByText(/70%/);

  fireEvent.click(screen.getByRole('link', { name: /edit career profile/i }));
  const nameField = await screen.findByLabelText(/full name/i);
  fireEvent.change(nameField, { target: { value: 'Unsaved Section Switch' } });

  fireEvent.click(screen.getByRole('tab', { name: /import review/i }));
  expect(screen.getByRole('heading', { name: /master cv/i })).toBeInTheDocument();
  expect(window.location.search).toBe('?section=import');

  fireEvent.click(screen.getByRole('tab', { name: /career profile/i }));
  expect(await screen.findByLabelText(/full name/i)).toHaveValue('Unsaved Section Switch');
  expect(screen.getByText('Unsaved changes')).toBeInTheDocument();
  expect(window.location.search).toBe('?section=profile');
});

test('pending CV extraction can be reviewed and applied', async () => {
  extractionRunsResponse = [{
    id: 13,
    trigger: 'upload',
    status: 'pending_review',
    artifactFileName: 'new-resume.pdf',
    startedAtUtc: '2026-03-29T12:00:00Z',
    completedAtUtc: '2026-03-29T12:00:05Z',
    parserVersion: 'm005-s01',
    normalizerVersion: 'm005-s01',
    llmPromptVersion: 'm005-s01',
  }];
  renderPage('import');

  expect(await screen.findByText(/3 additions/i)).toBeInTheDocument();
  const lowConfidence = screen.getByRole('checkbox', { name: /include low-confidence languages: french/i });
  expect(lowConfidence).not.toBeChecked();
  fireEvent.click(lowConfidence);
  fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));

  await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/runs/13/accept', {
    acceptedLowConfidenceIds: ['Languages|french'],
  }));
});

test('account email changes require a password and use the confirmation flow', async () => {
  mockedApi.post.mockImplementation((url: string) => {
    if (url === '/auth/email-change/request') return Promise.resolve({ data: { pendingEmail: 'new@example.com' } } as any);
    return Promise.resolve({ data: {} } as any);
  });
  renderWith(ProfilePage);

  const email = await screen.findByLabelText(/new email/i);
  fireEvent.change(email, { target: { value: 'new@example.com' } });
  fireEvent.change(screen.getByLabelText(/password to confirm email change/i), { target: { value: 'password1' } });
  fireEvent.click(screen.getByRole('button', { name: /send confirmation/i }));

  await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/email-change/request', {
    email: 'new@example.com',
    currentPassword: 'password1',
  }));
  expect(await screen.findByText(/waiting for confirmation from new@example.com/i)).toBeInTheDocument();
});
