// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. 

import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/vitest';
import App from './App';

// ===== Mocks =====

// Mock child components to avoid their complexity
vi.mock('./components/VisualizationPanel', () => {
  interface MockProps {
    nDiagram: Record<string, unknown> | null;
    n1Diagram: Record<string, unknown> | null;
    configLoading: boolean;
    layoutPath: string;
    networkPath: string;
    onOpenSettings: (tab: string) => void;
  }
  const MockVisualizationPanel = (props: MockProps) => {
    const { nDiagram, n1Diagram, configLoading, layoutPath, networkPath, onOpenSettings } = props;
    const [warningDismissed, setWarningDismissed] = React.useState(false);
    const hasAnyDiagram = !!nDiagram?.svg || !!n1Diagram?.svg;
    const showPathWarning = !warningDismissed && !hasAnyDiagram;

    return (
      <div
        data-testid="visualization-panel"
        data-n1-diagram-present={!!n1Diagram}
      >
        {!hasAnyDiagram && !configLoading && showPathWarning && (
          <div>
            <div>Configuration Paths</div>
            <button onClick={() => setWarningDismissed(true)}>✕</button>
            <div>Layout Path: {layoutPath}</div>
            <div>Output Folder: {networkPath ? (networkPath.includes('/') ? networkPath.substring(0, networkPath.lastIndexOf('/')) : networkPath) : 'Not set'}</div>
            <button onClick={() => onOpenSettings('paths')}>Change in settings</button>
          </div>
        )}
      </div>
    );
  };
  return { default: MockVisualizationPanel };
});
vi.mock('./components/ActionFeed', () => ({
  default: (props: { linesOverloaded: string[]; pendingAnalysisResult: object | null; analysisLoading: boolean; onDisplayPrioritizedActions: () => void; onRunAnalysis: () => void; canRunAnalysis: boolean }) => (
    <div
      data-testid="action-feed"
      data-ol-count={props.linesOverloaded?.length || 0}
      data-pending={!!props.pendingAnalysisResult}
      data-loading={!!props.analysisLoading}
    >
      {props.analysisLoading ? (
        <button disabled>⚙️ Analyzing…</button>
      ) : props.pendingAnalysisResult ? (
        <button onClick={props.onDisplayPrioritizedActions}>Display prioritized actions</button>
      ) : (
        <button onClick={props.onRunAnalysis} disabled={!props.canRunAnalysis}>🔍 Analyze & Suggest</button>
      )}
    </div>
  ),
}));
vi.mock('./components/OverloadPanel', () => ({
  default: (props: { n1Overloads: string[]; selectedOverloads: Set<string> }) => (
    <div
      data-testid="overload-panel"
      data-n1-ol-count={props.n1Overloads?.length || 0}
      data-sel-ol-count={props.selectedOverloads?.size || 0}
    />
  ),
}));

// Mock hooks
vi.mock('./hooks/usePanZoom', () => ({
  usePanZoom: () => ({ viewBox: null, setViewBox: vi.fn() }),
}));

// Mock SVG utilities
vi.mock('./utils/svgUtils', () => ({
  processSvg: (svg: string) => ({ svg, viewBox: { x: 0, y: 0, w: 100, h: 100 } }),
  buildMetadataIndex: () => null,
  applyOverloadedHighlights: vi.fn(),
  applyDeltaVisuals: vi.fn(),
  applyActionTargetHighlights: vi.fn(),
  applyContingencyHighlight: vi.fn(),
  getIdMap: () => new Map(),
  invalidateIdMapCache: vi.fn(),
  isCouplingAction: vi.fn(() => false),
  attachVlInteractions: vi.fn(() => () => {}),
}));


// Mock API — use vi.hoisted to define mock before vi.mock hoists
const mockApi = vi.hoisted(() => ({
  updateConfig: vi.fn().mockResolvedValue({ monitored_lines_count: 10, total_lines_count: 10 }),
  getBranches: vi.fn().mockResolvedValue({ branches: ['BRANCH_A', 'BRANCH_B', 'BRANCH_C'], name_map: {} }),
  getVoltageLevels: vi.fn().mockResolvedValue({ voltage_levels: ['VL1', 'VL2'], name_map: {} }),
  getNominalVoltages: vi.fn().mockResolvedValue({ mapping: {}, unique_kv: [63, 225] }),
  getVoltageLevelSubstations: vi.fn().mockResolvedValue({ mapping: {} }),
  getNetworkDiagram: vi.fn().mockResolvedValue({ svg: '<svg></svg>', metadata: null }),
  getContingencyDiagram: vi.fn().mockResolvedValue({ svg: '<svg></svg>', metadata: null, lines_overloaded: [] }),
  pickPath: vi.fn(),
  runAnalysisStep1: vi.fn().mockResolvedValue({ can_proceed: true, lines_overloaded: ['LINE_OL1'] }),
  runAnalysisStep2Stream: vi.fn(),
  getActionVariantDiagram: vi.fn().mockResolvedValue({ svg: '<svg></svg>', metadata: null }),
  getUserConfig: vi.fn().mockResolvedValue({
    network_path: '/home/user/data/grid.xiidm',
    action_file_path: '/home/user/data/actions.json'
  }),
  getConfigFilePath: vi.fn().mockResolvedValue('/home/user/data/config.json'),
  saveUserConfig: vi.fn().mockResolvedValue({}),
  setConfigFilePath: vi.fn().mockResolvedValue({ config_file_path: '/home/user/data/config.json', config: {} }),
  getNSld: vi.fn(),
  getContingencySld: vi.fn(),
  getActionVariantSld: vi.fn(),
}));

vi.mock('./api', () => ({
  api: mockApi,
}));

afterEach(() => {
  cleanup();
});

// Helper: render App, load config, wait for branches to appear
async function renderAndLoadStudy() {
  render(<App />);

  // Click Load Study
  const loadBtn = screen.getByText('🔄 Load Study');
  await userEvent.click(loadBtn);

  // Wait for branches to be loaded (which means handleLoadConfig is done)
  await waitFor(() => {
    expect(screen.getByText('⚡ Select Contingency')).toBeInTheDocument();
  }, { timeout: 5000 });
}

// Helper: pick ``branchName`` from the react-select multi-select
// then click the Trigger button to commit the contingency.
async function selectBranch(branchName: string) {
  const combobox = screen.getByRole('combobox');
  await act(async () => {
    await userEvent.click(combobox);
    await userEvent.type(combobox, branchName);
    await userEvent.keyboard('{Enter}');
  });
  const trigger = await screen.findByRole('button', { name: /Trigger/ });
  await act(async () => {
    await userEvent.click(trigger);
  });
  await waitFor(() => {
    expect(mockApi.getContingencyDiagram).toHaveBeenCalledWith([branchName]);
  });
}

// Helper: run analysis to create analysis state
async function runAnalysis() {
  // Mock runAnalysisStep2Stream to return a streaming Response
  const mockStream = new ReadableStream({
    start(controller) {
      controller.enqueue(new TextEncoder().encode(
        JSON.stringify({ type: 'result', actions: { ACT1: { is_manual: false, rho_before: [1.02], rho_after: [0.95] } }, lines_overloaded: ['LINE_OL1'], message: 'done', dc_fallback: false }) + '\n'
      ));
      controller.close();
    },
  });
  mockApi.runAnalysisStep2Stream.mockResolvedValue({
    ok: true,
    body: mockStream,
  });

  const runBtn = screen.getByText('🔍 Analyze & Suggest');
  await act(async () => {
    await userEvent.click(runBtn);
  });

  await waitFor(() => {
    const running = screen.queryByText('⚙️ Analyzing…');
    if (running) throw new Error('Still analyzing...');
  }, { timeout: 5000 });

  // Click Display Actions if present
  const displayBtn = await screen.findByText(/Display.*prioritized actions/, {}, { timeout: 3000 });
  await userEvent.click(displayBtn);
}

describe('Load Study Confirmation', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
  });

  it('loads study directly when no analysis state exists', async () => {
    render(<App />);

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    // No dialog, just loads
    expect(screen.queryByText('Reload Study?')).not.toBeInTheDocument();
    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
  });

  it('shows confirmation dialog when clicking Load Study after running analysis', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    mockApi.updateConfig.mockClear();

    // Click Load Study again
    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    // Dialog should appear
    await waitFor(() => {
      expect(screen.getByText('Reload Study?')).toBeInTheDocument();
    });
    expect(screen.getByText(/The network will be reloaded from scratch/)).toBeInTheDocument();

    // Config should NOT have been called yet
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
  });

  it('reloads study when user confirms Load Study dialog', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    mockApi.updateConfig.mockClear();

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    await waitFor(() => {
      expect(screen.getByText('Reload Study?')).toBeInTheDocument();
    });

    // Click Confirm
    await act(async () => {
      await userEvent.click(screen.getByText('Confirm'));
    });

    // Dialog should close and config should be called
    expect(screen.queryByText('Reload Study?')).not.toBeInTheDocument();
    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
  });

  it('keeps state when user cancels Load Study dialog', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    mockApi.updateConfig.mockClear();

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    await waitFor(() => {
      expect(screen.getByText('Reload Study?')).toBeInTheDocument();
    });

    // Click Cancel
    await act(async () => {
      await userEvent.click(screen.getByText('Cancel'));
    });

    // Dialog should close, config should NOT be called
    expect(screen.queryByText('Reload Study?')).not.toBeInTheDocument();
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
  });
});

describe('Full State Reset on Load Study', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
  });

  it('clears branch selection after Load Study with no prior analysis state', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');

    expect(screen.getByTestId('sidebar-summary-contingency').textContent).toContain('BRANCH_A');

    mockApi.updateConfig.mockClear();
    mockApi.getBranches.mockClear();

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    // No dialog — no analysis state
    expect(screen.queryByText('Reload Study?')).not.toBeInTheDocument();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
    await waitFor(() => {
      expect(mockApi.getBranches).toHaveBeenCalled();
    });

    // Branch input should be cleared
    expect(screen.queryByTestId('sidebar-summary-contingency')).toBeNull();
  });

  it('clears branch selection after confirming Load Study with analysis state', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    expect(screen.getByTestId('sidebar-summary-contingency').textContent).toContain('BRANCH_A');

    mockApi.updateConfig.mockClear();
    mockApi.getBranches.mockClear();

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    await waitFor(() => {
      expect(screen.getByText('Reload Study?')).toBeInTheDocument();
    });

    await act(async () => {
      await userEvent.click(screen.getByText('Confirm'));
    });

    expect(screen.queryByText('Reload Study?')).not.toBeInTheDocument();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
    await waitFor(() => {
      expect(mockApi.getBranches).toHaveBeenCalled();
    });

    // Branch input must be cleared after reset
    expect(screen.queryByTestId('sidebar-summary-contingency')).toBeNull();
  });

  it('re-fetches branches after Load Study reset', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');

    mockApi.getBranches.mockClear();

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    await waitFor(() => {
      expect(mockApi.getBranches).toHaveBeenCalled();
    });
  });

  it('preserves configuration paths across Load Study reset', async () => {
    await renderAndLoadStudy();

    const firstCallArgs = mockApi.updateConfig.mock.calls[0][0];
    expect(firstCallArgs.network_path).toBeTruthy();
    expect(firstCallArgs.action_file_path).toBeTruthy();

    mockApi.updateConfig.mockClear();

    const loadBtn = screen.getByText('🔄 Load Study');
    await act(async () => {
      await userEvent.click(loadBtn);
    });

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });

    const secondCallArgs = mockApi.updateConfig.mock.calls[0][0];
    expect(secondCallArgs.network_path).toBe(firstCallArgs.network_path);
    expect(secondCallArgs.action_file_path).toBe(firstCallArgs.action_file_path);
  });
});

describe('Full State Reset on Apply Settings', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
  });

  async function openSettings() {
    const settingsBtn = screen.getByTitle('Settings');
    await act(async () => {
      await userEvent.click(settingsBtn);
    });
    await waitFor(() => {
      expect(screen.getByText('Apply')).toBeInTheDocument();
    });
  }

  // Convenience: click Apply and confirm the resulting "Apply New
  // Settings?" dialog. With a study already loaded, every Apply now
  // routes through the confirmation pipeline (Bug "user warning when
  // changing config path while a network is loaded").
  async function applyAndConfirm() {
    await act(async () => {
      await userEvent.click(screen.getByText('Apply'));
    });
    await waitFor(() => {
      expect(screen.getByText('Apply New Settings?')).toBeInTheDocument();
    });
    await act(async () => {
      await userEvent.click(screen.getByText('Confirm'));
    });
  }

  it('clears branch selection after Apply Settings', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');

    expect(screen.getByTestId('sidebar-summary-contingency').textContent).toContain('BRANCH_A');

    mockApi.updateConfig.mockClear();

    await openSettings();
    await applyAndConfirm();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });

    expect(screen.queryByTestId('sidebar-summary-contingency')).toBeNull();
    expect(screen.queryByText('Apply')).not.toBeInTheDocument();
  });

  it('clears branch and analysis state after Apply Settings with analysis state', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    expect(screen.getByTestId('sidebar-summary-contingency').textContent).toContain('BRANCH_A');

    mockApi.updateConfig.mockClear();

    await openSettings();
    await applyAndConfirm();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });

    expect(screen.queryByTestId('sidebar-summary-contingency')).toBeNull();
    expect(screen.queryByText('Apply')).not.toBeInTheDocument();
  });

  it('closes settings modal after Apply Settings', async () => {
    await renderAndLoadStudy();
    await openSettings();

    expect(screen.getByText('Apply')).toBeInTheDocument();

    await applyAndConfirm();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });

    expect(screen.queryByText('Apply')).not.toBeInTheDocument();
  });

  it('calls updateConfig with current settings values after Apply Settings', async () => {
    await renderAndLoadStudy();

    mockApi.updateConfig.mockClear();

    await openSettings();
    await applyAndConfirm();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });

    const callArgs = mockApi.updateConfig.mock.calls[0][0];
    expect(callArgs).toHaveProperty('min_line_reconnections');
    expect(callArgs).toHaveProperty('monitoring_factor');
    expect(callArgs).toHaveProperty('n_prioritized_actions');
  });

  it('re-fetches branches after Apply Settings', async () => {
    await renderAndLoadStudy();

    mockApi.getBranches.mockClear();
    mockApi.updateConfig.mockClear();

    await openSettings();
    await applyAndConfirm();

    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });

    // Apply Settings DOES now re-fetch branches (matching Load Study behavior)
    expect(mockApi.getBranches).toHaveBeenCalled();
  });
});

describe('Apply Settings Confirmation', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
  });

  async function openSettings() {
    const settingsBtn = screen.getByTitle('Settings');
    await act(async () => {
      await userEvent.click(settingsBtn);
    });
    await waitFor(() => {
      expect(screen.getByText('Apply')).toBeInTheDocument();
    });
  }

  it('applies settings directly when no study has been loaded yet', async () => {
    // Brand-new app, no Load Study clicked. There's nothing to
    // discard, so Apply must not show the dialog.
    render(<App />);

    const settingsBtn = screen.getByTitle('Settings');
    await act(async () => {
      await userEvent.click(settingsBtn);
    });
    await waitFor(() => {
      expect(screen.getByText('Apply')).toBeInTheDocument();
    });

    mockApi.updateConfig.mockClear();
    await act(async () => {
      await userEvent.click(screen.getByText('Apply'));
    });

    // No confirmation dialog, settings apply immediately.
    expect(screen.queryByText('Apply New Settings?')).not.toBeInTheDocument();
    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
  });

  // Regression: changing a config-relevant setting (e.g. the config
  // file path) and clicking Apply while a network is loaded BUT no
  // analysis has been run must still warn the user, because Apply
  // unconditionally reloads the network and would silently drop the
  // currently-loaded grid.
  it('shows confirmation dialog when applying settings with a loaded network but no analysis', async () => {
    await renderAndLoadStudy();
    // Deliberately no selectBranch / no runAnalysis — only the base
    // network is loaded.

    mockApi.updateConfig.mockClear();
    await openSettings();

    // Type a new config file path (the typical user action this
    // request is about) before clicking Apply.
    const configPathInput = screen.getByLabelText(/Config File Path/i);
    await userEvent.clear(configPathInput);
    await userEvent.type(configPathInput, '/new/config.json');

    await act(async () => {
      await userEvent.click(screen.getByText('Apply'));
    });

    await waitFor(() => {
      expect(screen.getByText('Apply New Settings?')).toBeInTheDocument();
    });
    // Backend must NOT have been called yet — the user has to
    // confirm first.
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
  });

  it('shows confirmation dialog when applying settings after running analysis', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    mockApi.updateConfig.mockClear();
    await openSettings();

    await act(async () => {
      await userEvent.click(screen.getByText('Apply'));
    });

    // Dialog appears, with the apply-settings-specific copy.
    await waitFor(() => {
      expect(screen.getByText('Apply New Settings?')).toBeInTheDocument();
    });
    expect(
      screen.getByText(/The network will be reloaded with the new configuration/),
    ).toBeInTheDocument();

    // Backend must NOT have been called yet — applying is gated on
    // the user's confirmation.
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
  });

  it('proceeds with apply settings after confirmation', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    mockApi.updateConfig.mockClear();
    await openSettings();

    await act(async () => {
      await userEvent.click(screen.getByText('Apply'));
    });
    await waitFor(() => {
      expect(screen.getByText('Apply New Settings?')).toBeInTheDocument();
    });

    await act(async () => {
      await userEvent.click(screen.getByText('Confirm'));
    });

    // Dialog dismissed, settings applied, modal closed.
    expect(screen.queryByText('Apply New Settings?')).not.toBeInTheDocument();
    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
    expect(screen.queryByText('Apply')).not.toBeInTheDocument();
  });

  it('keeps state and modal open when user cancels apply settings dialog', async () => {
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    mockApi.updateConfig.mockClear();
    await openSettings();

    await act(async () => {
      await userEvent.click(screen.getByText('Apply'));
    });
    await waitFor(() => {
      expect(screen.getByText('Apply New Settings?')).toBeInTheDocument();
    });

    await act(async () => {
      await userEvent.click(screen.getByText('Cancel'));
    });

    // Dialog dismissed, no backend call, settings modal still open
    // (so the user can adjust their inputs without losing them).
    expect(screen.queryByText('Apply New Settings?')).not.toBeInTheDocument();
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
    expect(screen.getByText('Apply')).toBeInTheDocument();
    // The contingency selection must also still be intact.
    expect(screen.getByTestId('sidebar-summary-contingency').textContent).toContain('BRANCH_A');
  });
});

describe('Change Network Path Confirmation', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
  });

  it('does not prompt when no study has been loaded yet', async () => {
    // Fresh app, no Load Study click. Typing a new network path and
    // blurring must NOT show the dialog — there's nothing to discard.
    render(<App />);

    const input = await screen.findByTestId('header-network-path-input');
    await act(async () => {
      await userEvent.clear(input);
      await userEvent.type(input, '/tmp/freshly-chosen.xiidm');
      input.blur();
    });

    expect(screen.queryByText('Change Network?')).not.toBeInTheDocument();
  });

  it('shows confirmation dialog when typing a different path after a study is loaded', async () => {
    await renderAndLoadStudy();

    mockApi.updateConfig.mockClear();

    const input = screen.getByTestId('header-network-path-input');
    await act(async () => {
      await userEvent.clear(input);
      await userEvent.type(input, '/tmp/new-network.xiidm');
      input.blur();
    });

    await waitFor(() => {
      expect(screen.getByText('Change Network?')).toBeInTheDocument();
    });
    expect(
      screen.getByText(/The current study will be reloaded from the new network file/),
    ).toBeInTheDocument();
    // The backend must NOT have been called yet — confirmation is
    // what triggers the reload.
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
  });

  it('reloads the study with the new path on Confirm', async () => {
    await renderAndLoadStudy();

    mockApi.updateConfig.mockClear();
    mockApi.getBranches.mockClear();

    const input = screen.getByTestId('header-network-path-input');
    await act(async () => {
      await userEvent.clear(input);
      await userEvent.type(input, '/tmp/new-network.xiidm');
      input.blur();
    });

    await waitFor(() => {
      expect(screen.getByText('Change Network?')).toBeInTheDocument();
    });

    await act(async () => {
      await userEvent.click(screen.getByText('Confirm'));
    });

    // Dialog dismissed and a fresh load kicks in with the new path.
    expect(screen.queryByText('Change Network?')).not.toBeInTheDocument();
    await waitFor(() => {
      expect(mockApi.updateConfig).toHaveBeenCalled();
    });
    const lastCall = mockApi.updateConfig.mock.calls.at(-1)![0];
    expect(lastCall.network_path).toBe('/tmp/new-network.xiidm');
    expect(mockApi.getBranches).toHaveBeenCalled();
  });

  it('reverts the network path input on Cancel and keeps the current study', async () => {
    await renderAndLoadStudy();
    const initialPath = (screen.getByTestId('header-network-path-input') as HTMLInputElement).value;

    mockApi.updateConfig.mockClear();

    const input = screen.getByTestId('header-network-path-input');
    await act(async () => {
      await userEvent.clear(input);
      await userEvent.type(input, '/tmp/rejected.xiidm');
      input.blur();
    });

    await waitFor(() => {
      expect(screen.getByText('Change Network?')).toBeInTheDocument();
    });

    await act(async () => {
      await userEvent.click(screen.getByText('Cancel'));
    });

    // Dialog gone, no reload, and the Header field is back to the
    // path the currently-loaded study was loaded from.
    expect(screen.queryByText('Change Network?')).not.toBeInTheDocument();
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
    expect(
      (screen.getByTestId('header-network-path-input') as HTMLInputElement).value,
    ).toBe(initialPath);
  });

  it('does not prompt when blurring the input without actually changing the path', async () => {
    await renderAndLoadStudy();

    mockApi.updateConfig.mockClear();

    const input = screen.getByTestId('header-network-path-input');
    // Just focus and blur — same value committed.
    await act(async () => {
      input.focus();
      input.blur();
    });

    expect(screen.queryByText('Change Network?')).not.toBeInTheDocument();
    expect(mockApi.updateConfig).not.toHaveBeenCalled();
  });
});

// Regression: reloading an archived session whose contingency differs
// from the currently-loaded one must NOT trigger the "Change
// Contingency?" warning dialog. The dialog is only meant for the user-
// initiated contingency-swap gesture; a session restore is supposed to
// replace state wholesale, so the confirmation step would (a) be
// useless friction and (b) — worse — if the user confirms, the
// post-confirmation handler wipes the just-restored analysis result
// via `clearContingencyState()`, dropping every restored suggestion.
describe('Session reload — different contingency than current', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
    // The Reload Session flow needs an output folder to look in.
    mockApi.getUserConfig.mockResolvedValue({
      network_path: '/home/user/data/grid.xiidm',
      action_file_path: '/home/user/data/actions.json',
      output_folder_path: '/tmp/sessions',
    });
  });

  it('does NOT show "Change Contingency?" when restoring a session for a different contingency than currently loaded', async () => {
    // Mocks for the session reload API
    interface ApiWithSessionMocks {
      listSessions: ReturnType<typeof vi.fn>;
      loadSession: ReturnType<typeof vi.fn>;
      restoreAnalysisContext: ReturnType<typeof vi.fn>;
    }
    const apiAny = mockApi as unknown as ApiWithSessionMocks;
    apiAny.listSessions = vi.fn().mockResolvedValue({
      sessions: ['costudy4grid_session_BRANCH_B_2026-05-18T14-46-53'],
    });
    apiAny.restoreAnalysisContext = vi.fn().mockResolvedValue({
      status: 'success',
      lines_we_care_about_count: 0,
      computed_pairs_count: 0,
    });
    apiAny.loadSession = vi.fn().mockResolvedValue({
      saved_at: '2026-05-18T14:46:53.000Z',
      configuration: {
        network_path: '/home/user/data/grid.xiidm',
        action_file_path: '/home/user/data/actions.json',
        layout_path: '',
        min_line_reconnections: 2,
        min_close_coupling: 3,
        min_open_coupling: 2,
        min_line_disconnections: 3,
        min_pst: 1,
        min_load_shedding: 0,
        min_renewable_curtailment_actions: 0,
        n_prioritized_actions: 10,
        lines_monitoring_path: '',
        monitoring_factor: 0.95,
        pre_existing_overload_threshold: 0.02,
        ignore_reconnections: false,
        pypowsybl_fast_mode: true,
      },
      contingency: {
        disconnected_elements: ['BRANCH_B'],
        selected_overloads: [],
        monitor_deselected: false,
      },
      overloads: {
        n_overloads: [],
        n1_overloads: ['LINE_OL_RESTORED'],
        resolved_overloads: ['LINE_OL_RESTORED'],
      },
      overflow_graph: null,
      analysis: {
        message: 'restored',
        dc_fallback: false,
        action_scores: {},
        actions: {
          RESTORED_ACT_1: {
            description_unitaire: 'Restored action',
            rho_before: [1.05],
            rho_after: [0.92],
            max_rho: 0.92,
            max_rho_line: 'LINE_OL_RESTORED',
            is_rho_reduction: true,
            status: { is_selected: false, is_rejected: false, is_manually_simulated: false, is_suggested: true },
          },
        },
        combined_actions: {},
        lines_we_care_about: null,
        computed_pairs: null,
      },
    });

    // 1. Load study + commit BRANCH_A + run analysis so we have a
    //    populated result + non-empty selectedContingency + non-empty
    //    committedBranchRef.
    await renderAndLoadStudy();
    await selectBranch('BRANCH_A');
    await runAnalysis();

    // Verify pre-state: BRANCH_A committed and analysis result present.
    expect(screen.getByTestId('sidebar-summary-contingency').textContent).toContain('BRANCH_A');

    // 2. Open the Reload Session modal and pick the BRANCH_B session.
    const reloadBtn = screen.getByText('Reload Session');
    await act(async () => {
      await userEvent.click(reloadBtn);
    });
    const sessionEntry = await screen.findByText(/costudy4grid_session_BRANCH_B/);
    await act(async () => {
      await userEvent.click(sessionEntry);
    });

    // 3. Wait for the restore to settle.
    await waitFor(() => {
      expect(apiAny.loadSession).toHaveBeenCalled();
    });

    // 4. The "Change Contingency?" dialog MUST NOT appear. Before the
    //    fix this assertion failed because the useContingencyFetch
    //    effect saw the new selectedContingency=[BRANCH_B] alongside
    //    the just-restored result and fired the contingency-warning
    //    dialog — which on confirm wipes the restored suggestions via
    //    clearContingencyState.
    await waitFor(() => {
      expect(screen.queryByText('Change Contingency?')).not.toBeInTheDocument();
    });
  });

  // Second-reload regression: clicking Reload Session a SECOND time —
  // when the previous reload has already populated `result`,
  // `committedBranchRef` and `selectedContingency` — must still
  // suppress the contingency-change dialog. The defensive guard added
  // to resetAllState (which wipes per-study state at the START of
  // every restore + dismisses any stale confirmDialog) closes this
  // race window.
  it('does NOT show "Change Contingency?" on a SECOND consecutive reload of a different session', async () => {
    interface ApiWithSessionMocks {
      listSessions: ReturnType<typeof vi.fn>;
      loadSession: ReturnType<typeof vi.fn>;
      restoreAnalysisContext: ReturnType<typeof vi.fn>;
    }
    const apiAny = mockApi as unknown as ApiWithSessionMocks;
    apiAny.listSessions = vi.fn().mockResolvedValue({
      sessions: [
        'costudy4grid_session_BRANCH_B_2026-05-18T14-46-53',
        'costudy4grid_session_BRANCH_A_2026-05-18T15-12-04',
      ],
    });
    apiAny.restoreAnalysisContext = vi.fn().mockResolvedValue({
      status: 'success',
      lines_we_care_about_count: 0,
      computed_pairs_count: 0,
    });
    const sessionPayload = (branch: string) => ({
      saved_at: '2026-05-18T14:46:53.000Z',
      configuration: {
        network_path: '/home/user/data/grid.xiidm',
        action_file_path: '/home/user/data/actions.json',
        layout_path: '',
        min_line_reconnections: 2,
        min_close_coupling: 3,
        min_open_coupling: 2,
        min_line_disconnections: 3,
        min_pst: 1,
        min_load_shedding: 0,
        min_renewable_curtailment_actions: 0,
        n_prioritized_actions: 10,
        lines_monitoring_path: '',
        monitoring_factor: 0.95,
        pre_existing_overload_threshold: 0.02,
        ignore_reconnections: false,
        pypowsybl_fast_mode: true,
      },
      contingency: {
        disconnected_elements: [branch],
        selected_overloads: [],
        monitor_deselected: false,
      },
      overloads: {
        n_overloads: [],
        n1_overloads: ['LINE_OL_RESTORED'],
        resolved_overloads: ['LINE_OL_RESTORED'],
      },
      overflow_graph: null,
      analysis: {
        message: 'restored',
        dc_fallback: false,
        action_scores: {},
        actions: {
          [`RESTORED_ACT_${branch}`]: {
            description_unitaire: `Restored action for ${branch}`,
            rho_before: [1.05],
            rho_after: [0.92],
            max_rho: 0.92,
            max_rho_line: 'LINE_OL_RESTORED',
            is_rho_reduction: true,
            status: { is_selected: false, is_rejected: false, is_manually_simulated: false, is_suggested: true },
          },
        },
        combined_actions: {},
        lines_we_care_about: null,
        computed_pairs: null,
      },
    });
    apiAny.loadSession = vi.fn().mockImplementation(async (_folder: string, name: string) => {
      const branch = name.includes('BRANCH_B') ? 'BRANCH_B' : 'BRANCH_A';
      return sessionPayload(branch);
    });

    await renderAndLoadStudy();

    // First reload — BRANCH_B.
    const reloadBtn = screen.getByText('Reload Session');
    await act(async () => {
      await userEvent.click(reloadBtn);
    });
    const firstEntry = await screen.findByText(/costudy4grid_session_BRANCH_B/);
    await act(async () => {
      await userEvent.click(firstEntry);
    });
    await waitFor(() => {
      expect(apiAny.loadSession).toHaveBeenCalledTimes(1);
    });
    // First reload must not have left a stale dialog.
    expect(screen.queryByText('Change Contingency?')).not.toBeInTheDocument();

    // Second reload — BRANCH_A. The pre-existing refs from the first
    // reload (committedBranchRef=BRANCH_B, result=non-null) are the
    // exact race conditions that re-fired the dialog before the
    // resetAllState-at-top-of-restore fix.
    await act(async () => {
      await userEvent.click(screen.getByText('Reload Session'));
    });
    const secondEntry = await screen.findByText(/costudy4grid_session_BRANCH_A/);
    await act(async () => {
      await userEvent.click(secondEntry);
    });
    await waitFor(() => {
      expect(apiAny.loadSession).toHaveBeenCalledTimes(2);
    });

    await waitFor(() => {
      expect(screen.queryByText('Change Contingency?')).not.toBeInTheDocument();
    });
  });
});
