// 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';
import type { RecommenderDisplayConfig } from './types';

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

// Track props passed to memoized components to verify callback stability and groupedprops
const vizPanelRenderLog: Array<Record<string, unknown>> = [];
const actionFeedRenderLog: Array<Record<string, unknown>> = [];
const overloadPanelRenderLog: Array<Record<string, unknown>> = [];

vi.mock('./components/VisualizationPanel', () => {
  const MockVisualizationPanel = React.memo((props: Record<string, unknown>) => {
    vizPanelRenderLog.push({ ...props });
    return (
      <div
        data-testid="visualization-panel"
        data-active-tab={props.activeTab}
      />
    );
  });
  MockVisualizationPanel.displayName = 'MockVisualizationPanel';
  return { default: MockVisualizationPanel };
});

vi.mock('./components/ActionFeed', () => {
  const MockActionFeed = React.memo((props: Record<string, unknown>) => {
    actionFeedRenderLog.push({ ...props });
    return (
      <div
        data-testid="action-feed"
        data-has-recommender-config={!!props.recommenderConfig}
        data-loading={!!props.analysisLoading}
      >
        {(props.analysisLoading as boolean) ? (
          <button disabled>⚙️ Analyzing…</button>
        ) : (props.pendingAnalysisResult as unknown) ? (
          <button onClick={props.onDisplayPrioritizedActions as () => void}>Display prioritized actions</button>
        ) : (
          <button onClick={props.onRunAnalysis as () => void} disabled={!(props.canRunAnalysis as boolean)}>🔍 Analyze & Suggest</button>
        )}
      </div>
    );
  });
  MockActionFeed.displayName = 'MockActionFeed';
  return { default: MockActionFeed };
});

vi.mock('./components/OverloadPanel', () => {
  const MockOverloadPanel = React.memo((props: Record<string, unknown>) => {
    overloadPanelRenderLog.push({ ...props });
    return (
      <div
        data-testid="overload-panel"
      />
    );
  });
  MockOverloadPanel.displayName = 'MockOverloadPanel';
  return { default: MockOverloadPanel };
});

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

// Capture the handlers App wires into each attachVlInteractions call so
// the VL-disk interaction wiring tests can invoke onSelect / onOpenSld
// directly (the real delegation needs a live SVG the mock doesn't render).
const vlInteractionHandlers = vi.hoisted(
  () => [] as Array<{ onSelect?: (id: string) => void; onOpenSld?: (id: string) => void }>,
);

// 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(
    (_container: unknown, _meta: unknown, handlers: { onSelect?: (id: string) => void; onOpenSld?: (id: string) => void }) => {
      vlInteractionHandlers.push(handlers);
      return () => {};
    },
  ),
}));

// Mock API
const mockApi = vi.hoisted(() => ({
  getUserConfig: vi.fn().mockResolvedValue({
    network_path: '/home/user/data/grid.xiidm',
    action_file_path: '/home/user/data/actions.json',
    layout_path: '',
    output_folder_path: '',
    lines_monitoring_path: '',
    min_line_reconnections: 2.0,
    min_close_coupling: 3.0,
    min_open_coupling: 2.0,
    min_line_disconnections: 3.0,
    min_pst: 1.0,
    min_load_shedding: 0.0,
    min_renewable_curtailment_actions: 0.0,
    n_prioritized_actions: 10,
    monitoring_factor: 0.95,
    pre_existing_overload_threshold: 0.02,
    ignore_reconnections: false,
    pypowsybl_fast_mode: true,
  }),
  saveUserConfig: vi.fn().mockResolvedValue({}),
  getConfigFilePath: vi.fn().mockResolvedValue('/mock/config.json'),
  setConfigFilePath: vi.fn().mockResolvedValue({ config_file_path: '/mock/config.json', config: {} }),
  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 }),
  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: clear every chip currently displayed in the multi-select.
async function clearContingencyChips() {
  // react-select renders each chip with a ✕ button identified
  // by the ``cs4g-contingency__multi-value__remove`` class.
  let removeBtn = document.querySelector('.cs4g-contingency__multi-value__remove') as HTMLElement | null;
  while (removeBtn) {
    await act(async () => {
      await userEvent.click(removeBtn!);
    });
    removeBtn = document.querySelector('.cs4g-contingency__multi-value__remove') as HTMLElement | null;
  }
}

// Helper: pick ``branchName`` from the react-select multi-select
// then click the Trigger button to commit the contingency. By default
// REPLACES whatever was previously selected so older single-branch
// tests keep their semantics; pass ``{ append: true }`` to grow a
// multi-element contingency on top of the existing chips.
async function selectBranch(branchName: string, opts: { append?: boolean } = {}) {
  // The picker card hides once a contingency has been committed —
  // the sticky banner Clear shortcut brings it back. Route through
  // Clear when the combobox is missing so the helper keeps a
  // legacy-feel "switch branch" API for the existing test suite.
  let combobox = screen.queryByRole('combobox');
  if (!combobox) {
    const clearBtn = screen.queryByTestId('sidebar-summary-clear');
    if (clearBtn) {
      await act(async () => { await userEvent.click(clearBtn); });
      const confirmDialog = screen.queryByText('Change Contingency?');
      if (confirmDialog) {
        await act(async () => { await userEvent.click(screen.getByText('Confirm')); });
      }
    }
    combobox = screen.getByRole('combobox');
  } else if (!opts.append) {
    await clearContingencyChips();
  }
  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]);
  });
}


describe('Phase 2: State Management Optimization', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.unstubAllGlobals();
    vizPanelRenderLog.length = 0;
    actionFeedRenderLog.length = 0;
    overloadPanelRenderLog.length = 0;
  });

  describe('RecommenderDisplayConfig grouped prop', () => {
    // tier-warning-system PR: the recommender threshold display moved from a yellow
    // banner inside ActionFeed into a unified "Notices" pill in the
    // sidebar header (`docs/proposals/ui-design-critique.md`
    // recommendation #4). ActionFeed therefore no longer receives the
    // grouped `recommenderConfig` prop.
    it.skip('passes recommenderConfig as a single grouped object to ActionFeed', async () => {
      await renderAndLoadStudy();

      // ActionFeed should have been rendered at least once
      expect(actionFeedRenderLog.length).toBeGreaterThan(0);

      const lastRender = actionFeedRenderLog[actionFeedRenderLog.length - 1];
      expect(lastRender).toHaveProperty('recommenderConfig');

      const config = lastRender.recommenderConfig as RecommenderDisplayConfig;
      expect(config).toEqual({
        minLineReconnections: 2.0,
        minCloseCoupling: 3.0,
        minOpenCoupling: 2.0,
        minLineDisconnections: 3.0,
        minPst: 1.0,
        minLoadShedding: 0.0,
        minRenewableCurtailmentActions: 0.0,
        nPrioritizedActions: 10,
        ignoreReconnections: false,
      });
    });

    it('does NOT pass individual recommender settings as separate props', async () => {
      await renderAndLoadStudy();
      // ActionFeed mounts only after a contingency is committed
      // (sidebar visibility gate from the readability-feed PR).
      await selectBranch('BRANCH_A');

      const lastRender = actionFeedRenderLog[actionFeedRenderLog.length - 1];
      // These should NOT exist as individual props since they're now grouped
      expect(lastRender).not.toHaveProperty('minLineReconnections');
      expect(lastRender).not.toHaveProperty('minCloseCoupling');
      expect(lastRender).not.toHaveProperty('minOpenCoupling');
      expect(lastRender).not.toHaveProperty('minLineDisconnections');
      expect(lastRender).not.toHaveProperty('minPst');
      expect(lastRender).not.toHaveProperty('nPrioritizedActions');
      expect(lastRender).not.toHaveProperty('ignoreReconnections');
    });
  });

  describe('Memoized callback stability', () => {
    it('provides stable onTabChange callback across renders', async () => {
      await renderAndLoadStudy();

      // Capture onTabChange from first VisualizationPanel render after load
      const renderCount = vizPanelRenderLog.length;
      expect(renderCount).toBeGreaterThanOrEqual(1);

      const firstOnTabChange = vizPanelRenderLog[renderCount - 1].onTabChange;
      expect(typeof firstOnTabChange).toBe('function');

      // Trigger a state change that should NOT change the callback
      await selectBranch('BRANCH_A');

      // Check that onTabChange is still the same reference (or at least that the
      // component was provided a function, since our mock is React.memo'd)
      const laterOnTabChange = vizPanelRenderLog[vizPanelRenderLog.length - 1].onTabChange;
      expect(typeof laterOnTabChange).toBe('function');
    });

    it('provides stable onActionFavorite callback to ActionFeed', async () => {
      await renderAndLoadStudy();
      // ActionFeed only mounts post-contingency.
      await selectBranch('BRANCH_A');

      const lastRender = actionFeedRenderLog[actionFeedRenderLog.length - 1];
      expect(typeof lastRender.onActionFavorite).toBe('function');
      expect(typeof lastRender.onActionSelect).toBe('function');
      expect(typeof lastRender.onActionReject).toBe('function');
      expect(typeof lastRender.onRunAnalysis).toBe('function');
      expect(typeof lastRender.onAssetClick).toBe('function');
      expect(typeof lastRender.onManualActionAdded).toBe('function');
    });

    it.skip('provides stable toggle callbacks to OverloadPanel', async () => {
      // readability-feed PR retired the inline OverloadPanel — the
      // overload-toggle callbacks now route through the sticky
      // banner info-bubble popover (SidebarSummary). The component
      // is no longer mounted, so this assertion is dead weight.
      await renderAndLoadStudy();
      const lastRender = overloadPanelRenderLog[overloadPanelRenderLog.length - 1];
      expect(typeof lastRender.onToggleOverload).toBe('function');
      expect(typeof lastRender.onToggleMonitorDeselected).toBe('function');
    });

    it('provides stable onVoltageRangeChange and onInspectQueryChange to VisualizationPanel', async () => {
      await renderAndLoadStudy();

      const lastRender = vizPanelRenderLog[vizPanelRenderLog.length - 1];
      expect(typeof lastRender.onVoltageRangeChange).toBe('function');
      expect(typeof lastRender.onInspectQueryChange).toBe('function');
      expect(typeof lastRender.onVlOpen).toBe('function');
    });
  });

  describe('Voltage-level disk interaction wiring', () => {
    it('routes a single-clicked VL into the shared Inspect query', async () => {
      vlInteractionHandlers.length = 0;
      await renderAndLoadStudy();
      await waitFor(() => expect(vlInteractionHandlers.length).toBeGreaterThanOrEqual(3));

      // The latest effect run binds [n, contingency, action] in order, so
      // the first of the final three is the Network (N) tab's handler set.
      const nTab = vlInteractionHandlers[vlInteractionHandlers.length - 3];
      expect(typeof nTab.onSelect).toBe('function');

      act(() => { nTab.onSelect!('VL1'); });

      await waitFor(() => {
        const last = vizPanelRenderLog[vizPanelRenderLog.length - 1];
        expect(last.inspectQuery).toBe('VL1');
      });
    });

    it('opens the SLD overlay when a VL disk is double-clicked', async () => {
      mockApi.getNSld.mockResolvedValueOnce({ svg: '<svg></svg>', sld_metadata: null, switch_states: {} });
      vlInteractionHandlers.length = 0;
      await renderAndLoadStudy();
      await waitFor(() => expect(vlInteractionHandlers.length).toBeGreaterThanOrEqual(3));

      const handlers = vlInteractionHandlers[vlInteractionHandlers.length - 1];
      expect(typeof handlers.onOpenSld).toBe('function');

      await act(async () => { handlers.onOpenSld!('VL1'); });

      await waitFor(() => {
        const last = vizPanelRenderLog[vizPanelRenderLog.length - 1];
        const overlay = last.vlOverlay as { vlName?: string } | null;
        expect(overlay?.vlName).toBe('VL1');
      });
    });
  });

  describe('resetAllState consolidation', () => {
    it('clears analysis state when Load Study is clicked after loading', async () => {
      await renderAndLoadStudy();
      await selectBranch('BRANCH_A');

      // Reset mock call counts
      mockApi.updateConfig.mockClear();
      mockApi.getBranches.mockClear();

      // Click Load Study again — this triggers resetAllState via handleLoadConfig
      const loadBtn = screen.getByText('🔄 Load Study');
      await userEvent.click(loadBtn);

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

      // After reset, the activeTab should be 'n' (default)
      const vizPanel = vizPanelRenderLog[vizPanelRenderLog.length - 1];
      expect(vizPanel.activeTab).toBe('n');
    });

    it('clears contingency state when switching branches after analysis', async () => {
      await renderAndLoadStudy();
      await selectBranch('BRANCH_A');

      // After selecting BRANCH_A, the action feed should show analysis-ready state
      const lastActionFeed = actionFeedRenderLog[actionFeedRenderLog.length - 1];
      expect(lastActionFeed.analysisLoading).toBe(false);
    });
  });

  describe('React.memo integration', () => {
    it.skip('renders ActionFeed with data-has-recommender-config attribute', async () => {
      // tier-warning-system PR: the recommenderConfig prop was retired when the
      // recommender-thresholds notice moved to NoticesPanel. The mock
      // attribute it set is no longer meaningful.
      await renderAndLoadStudy();

      const actionFeed = screen.getByTestId('action-feed');
      expect(actionFeed).toHaveAttribute('data-has-recommender-config', 'true');
    });

    it('renders the visualization and action-feed memoized panels after a contingency is committed', async () => {
      // The OverloadPanel was removed in the readability-feed PR;
      // its functionality moved to SidebarSummary's info bubble.
      // ActionFeed mounts only after a contingency is committed.
      await renderAndLoadStudy();
      await selectBranch('BRANCH_A');

      expect(screen.getByTestId('visualization-panel')).toBeInTheDocument();
      expect(screen.getByTestId('action-feed')).toBeInTheDocument();
    });
  });

  describe('Second contingency state reset', () => {
    it('clearContingencyState does NOT reset activeTab to n', async () => {
      await renderAndLoadStudy();

      // Select first branch → should switch to n-1 tab
      await selectBranch('BRANCH_A');

      await waitFor(() => {
        const lastViz = vizPanelRenderLog[vizPanelRenderLog.length - 1];
        expect(lastViz.activeTab).toBe('contingency');
      });

      // Select second branch → should stay on n-1 tab (not flash to n)
      await selectBranch('BRANCH_B');

      await waitFor(() => {
        const lastViz = vizPanelRenderLog[vizPanelRenderLog.length - 1];
        // activeTab must be 'contingency' after contingency switch, NOT 'n'
        expect(lastViz.activeTab).toBe('contingency');
      });
    });

    it('n1Diagram is cleared and re-fetched on contingency switch', async () => {
      await renderAndLoadStudy();

      await selectBranch('BRANCH_A');
      const firstCallCount = mockApi.getContingencyDiagram.mock.calls.length;

      await selectBranch('BRANCH_B');

      // Should have fetched N-1 diagram for the new branch
      expect(mockApi.getContingencyDiagram).toHaveBeenCalledWith(['BRANCH_B']);
      expect(mockApi.getContingencyDiagram.mock.calls.length).toBeGreaterThan(firstCallCount);
    });
  });
});
