// 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 { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, within } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import ExplorePairsTab from './ExplorePairsTab';
import type { AnalysisResult, CombinedAction } from '../types';

describe('ExplorePairsTab', () => {
    const scoredActionsList = [
        { actionId: 'act1', score: 10, type: 'disco', mwStart: null },
        { actionId: 'act2', score: 20, type: 'reco', mwStart: 50.0 },
        { actionId: 'act3', score: 15, type: 'disco', mwStart: null },
    ];

    const mockAnalysisResult: AnalysisResult = {
        actions: {
            act1: { description_unitaire: 'D1', max_rho: 0.8, rho_before: [0.8], rho_after: [0.7], max_rho_line: 'L1', is_rho_reduction: true, action_topology: { lines_ex_bus: {}, lines_or_bus: {}, gens_bus: {}, loads_bus: {} } },
            act2: { description_unitaire: 'R1', max_rho: 0.9, rho_before: [0.9], rho_after: [0.8], max_rho_line: 'L2', is_rho_reduction: true, action_topology: { lines_ex_bus: {}, lines_or_bus: {}, gens_bus: {}, loads_bus: {} } },
        },
        lines_overloaded: [],
        message: 'done',
        dc_fallback: false,
        pdf_path: null,
        pdf_url: null,
    };

    const defaultProps = {
        scoredActionsList,
        selectedIds: new Set<string>(),
        onToggle: vi.fn(),
        onClearSelection: vi.fn(),
        preview: null as CombinedAction | null,
        simulationFeedback: null,
        sessionSimResults: {} as Record<string, { max_rho: number | null; max_rho_line: string; is_rho_reduction: boolean }>,
        analysisResult: mockAnalysisResult,
        loading: false,
        error: null as string | null,
        simulating: false,
        hasRestricted: false,
        monitoringFactor: 0.95,
        onEstimate: vi.fn(),
        onSimulate: vi.fn(),
        onSimulateSingle: vi.fn(),
    };

    it('renders selection chips header showing 0/2 selected', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getByText('Selected Actions (0/2)')).toBeInTheDocument();
    });

    it('renders placeholder text when no actions selected', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getByText(/Click rows in the table below to select/)).toBeInTheDocument();
    });

    it('renders selected chips when actions are selected', () => {
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} />);
        expect(screen.getByTestId('chip-act1')).toBeInTheDocument();
        expect(screen.getByTestId('chip-act2')).toBeInTheDocument();
        expect(screen.getByText('Selected Actions (2/2)')).toBeInTheDocument();
    });

    it('renders action rows from scored actions list', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getByText('act1')).toBeInTheDocument();
        expect(screen.getByText('act2')).toBeInTheDocument();
        expect(screen.getByText('act3')).toBeInTheDocument();
    });

    it('calls onToggle when an action row is clicked', () => {
        const onToggle = vi.fn();
        render(<ExplorePairsTab {...defaultProps} onToggle={onToggle} />);
        fireEvent.click(screen.getByText('act1'));
        expect(onToggle).toHaveBeenCalledWith('act1');
    });

    it('shows estimate button when less than 2 actions selected', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getByText('Select 2 actions to estimate')).toBeInTheDocument();
    });

    it('shows enabled estimate button when 2 actions selected', () => {
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} />);
        expect(screen.getByTestId('estimate-button')).toBeInTheDocument();
        expect(screen.getByText('Estimate combination effect')).toBeInTheDocument();
    });

    it('disables Estimate and surfaces the load-shedding/curtailment caveat when hasRestricted is true', () => {
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} hasRestricted={true} />);
        const estimateBtn = screen.getByTestId('estimate-button');
        expect(estimateBtn).toBeDisabled();
        expect(estimateBtn).toHaveTextContent(/Estimation not available/i);
    });

    it('exposes a Simulate Combined button in the no-preview state that stays enabled even when hasRestricted', () => {
        const onSimulate = vi.fn();
        render(
            <ExplorePairsTab
                {...defaultProps}
                selectedIds={new Set(['act1', 'act2'])}
                hasRestricted={true}
                onSimulate={onSimulate}
            />,
        );
        const simBtn = screen.getByTestId('simulate-combined-button');
        expect(simBtn).toBeEnabled();
        fireEvent.click(simBtn);
        expect(onSimulate).toHaveBeenCalled();
    });

    it('disables the no-preview Simulate Combined button until 2 actions are selected', () => {
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1'])} />);
        expect(screen.getByTestId('simulate-combined-button')).toBeDisabled();
    });

    it('calls onEstimate when estimate button is clicked', () => {
        const onEstimate = vi.fn();
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} onEstimate={onEstimate} />);
        fireEvent.click(screen.getByTestId('estimate-button'));
        expect(onEstimate).toHaveBeenCalled();
    });

    it('shows estimating message when loading', () => {
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} loading={true} />);
        expect(screen.getByText(/Estimating Combination/)).toBeInTheDocument();
    });

    it('renders comparison card when preview is set', () => {
        const preview: CombinedAction = {
            action1_id: 'act1',
            action2_id: 'act2',
            betas: [1.0, 0.9],
            max_rho: 0.72,
            max_rho_line: 'L3',
            estimated_max_rho: 0.75,
            estimated_max_rho_line: 'L3',
            is_rho_reduction: true,
            description: 'Combined',
            p_or_combined: [],
            rho_before: [0.8],
            rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} />);
        expect(screen.getByTestId('comparison-card')).toBeInTheDocument();
        expect(screen.getByText('75.0%')).toBeInTheDocument();
        expect(screen.getByText(/Estimated Max Loading/)).toBeInTheDocument();
    });

    it('shows simulation feedback in comparison card', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        const feedback = { max_rho: 0.68, max_rho_line: 'L2', is_rho_reduction: true };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} simulationFeedback={feedback} />);
        expect(screen.getByTestId('simulation-feedback')).toBeInTheDocument();
        expect(screen.getByText('68.0%')).toBeInTheDocument();
    });

    it('shows the Simulate Combined button on top of the card while a preview is shown and no simulation has run yet', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} />);
        expect(screen.getByTestId('simulate-combined-top-button')).toBeInTheDocument();
    });

    it('hides the top Simulate Combined button once a simulation result is available', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        const feedback = { max_rho: 0.68, max_rho_line: 'L2', is_rho_reduction: true };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} simulationFeedback={feedback} />);
        expect(screen.queryByTestId('simulate-combined-top-button')).not.toBeInTheDocument();
    });

    it('renders the card with only the Simulation Result column when simulation ran without an estimate', () => {
        const feedback = { max_rho: 0.68, max_rho_line: 'L2', is_rho_reduction: true };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} simulationFeedback={feedback} />);
        expect(screen.getByTestId('comparison-card')).toBeInTheDocument();
        expect(screen.getByTestId('simulation-feedback')).toBeInTheDocument();
        expect(screen.queryByText(/Estimated Max Loading/)).not.toBeInTheDocument();
    });

    it('does not render Estimate / Simulate buttons inside the comparison card header', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} />);
        const card = screen.getByTestId('comparison-card');
        expect(within(card).queryByText('Estimate combination effect')).not.toBeInTheDocument();
        expect(within(card).queryByText('Estimation unavailable')).not.toBeInTheDocument();
        expect(within(card).queryByText('Simulate Combined')).not.toBeInTheDocument();
    });

    it('hides the bottom action bar once a simulation has run (no preview path)', () => {
        const feedback = { max_rho: 0.68, max_rho_line: 'L2', is_rho_reduction: true };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} simulationFeedback={feedback} />);
        expect(screen.queryByTestId('estimate-button')).not.toBeInTheDocument();
        expect(screen.queryByTestId('simulate-combined-button')).not.toBeInTheDocument();
    });

    it('clicking the top Simulate Combined button calls onSimulate', () => {
        const onSimulate = vi.fn();
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} onSimulate={onSimulate} />);
        fireEvent.click(screen.getByTestId('simulate-combined-top-button'));
        expect(onSimulate).toHaveBeenCalled();
    });

    it('disables the top Simulate Combined button while simulating', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} simulating={true} />);
        expect(screen.getByTestId('simulate-combined-top-button')).toBeDisabled();
    });

    it('uses the "Simulation Result" title when only simulation ran (no estimate)', () => {
        const feedback = { max_rho: 0.68, max_rho_line: 'L2', is_rho_reduction: true };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} simulationFeedback={feedback} />);
        const card = screen.getByTestId('comparison-card');
        // "Simulation Result" appears as both the card title and the column header — so we expect 2 matches
        expect(within(card).getAllByText('Simulation Result').length).toBe(2);
        expect(within(card).queryByText('Explore Pairs Comparison')).not.toBeInTheDocument();
    });

    it('surfaces target_max_rho in the comparison card when it differs from the global estimated line', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.5, 0.5],
            max_rho: 0.82, max_rho_line: 'LOUHAL31PYMON',
            estimated_max_rho: 0.82, estimated_max_rho_line: 'LOUHAL31PYMON',
            target_max_rho: 0.20, target_max_rho_line: 'BEON L31CPVAN',
            is_rho_reduction: false, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.20],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} />);
        const target = screen.getByTestId('target-max-rho');
        expect(target).toHaveTextContent(/20\.0%.*BEON L31CPVAN/);
    });

    it('hides target_max_rho when it matches the global estimated line (nothing to add)', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0, 0.9],
            max_rho: 0.72, max_rho_line: 'L3',
            estimated_max_rho: 0.72, estimated_max_rho_line: 'L3',
            target_max_rho: 0.72, target_max_rho_line: 'L3',
            is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} />);
        expect(screen.queryByTestId('target-max-rho')).not.toBeInTheDocument();
    });

    it('uses the "Explore Pairs Comparison" title when an estimate is present', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} />);
        const card = screen.getByTestId('comparison-card');
        expect(within(card).getByText('Explore Pairs Comparison')).toBeInTheDocument();
    });

    it('shows error message in comparison card', () => {
        const preview: CombinedAction = {
            action1_id: 'act1', action2_id: 'act2', betas: [1.0], max_rho: 0.72,
            max_rho_line: 'L3', is_rho_reduction: true, description: 'Combined',
            p_or_combined: [], rho_before: [0.8], rho_after: [0.72],
        };
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1', 'act2'])} preview={preview} error="Superposition failed" />);
        expect(screen.getByText('Superposition failed')).toBeInTheDocument();
    });

    // The action-type / severity filtering moved OUT of this tab — it
    // is now driven by the shared ActionFilterRings in the Combine-modal
    // header, which hands ExplorePairsTab an already-filtered
    // `scoredActionsList`. The tab itself only renders what it is given.
    it('renders exactly the rows it is handed (no in-tab filtering)', () => {
        render(<ExplorePairsTab {...defaultProps} scoredActionsList={[scoredActionsList[0]]} />);
        expect(screen.getByText('act1')).toBeInTheDocument();
        expect(screen.queryByText('act2')).not.toBeInTheDocument();
        expect(screen.queryByText('act3')).not.toBeInTheDocument();
    });

    it('calls onClearSelection when Clear All is clicked', () => {
        const onClearSelection = vi.fn();
        render(<ExplorePairsTab {...defaultProps} selectedIds={new Set(['act1'])} onClearSelection={onClearSelection} />);
        fireEvent.click(screen.getByText('Clear All'));
        expect(onClearSelection).toHaveBeenCalled();
    });

    it('shows MW Start values in action rows', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getByText('50.0')).toBeInTheDocument();
    });

    it('shows N/A for null MW Start values', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getAllByText('N/A').length).toBeGreaterThan(0);
    });

    it('shows the empty state when handed an empty scored-actions list', () => {
        render(<ExplorePairsTab {...defaultProps} scoredActionsList={[]} />);
        expect(screen.getByText(/No scored actions available/)).toBeInTheDocument();
    });

    it('renders every row it is handed', () => {
        render(<ExplorePairsTab {...defaultProps} />);
        expect(screen.getByText('act1')).toBeInTheDocument();
        expect(screen.getByText('act2')).toBeInTheDocument();
        expect(screen.getByText('act3')).toBeInTheDocument();
    });

    // Injection actions (load shedding / curtailment / redispatch) expose an
    // editable MW setpoint per row so the operator can tune the target shed
    // amount or the signed redispatch delta before simulating — parity with
    // the manual-selection table and the action card.
    describe('editable injection setpoint', () => {
        const injectionList = [
            { actionId: 'load_shedding_L1', score: 22, type: 'load_shedding', mwStart: 22.0 },
            { actionId: 'redispatch_G1', score: 1.0, type: 'redispatch', mwStart: 14.3 },
        ];
        const injectionAnalysis: AnalysisResult = {
            actions: {
                redispatch_G1: {
                    description_unitaire: 'redispatch G1', max_rho: 0.9, rho_before: [0.9], rho_after: [0.8],
                    max_rho_line: 'L1', is_rho_reduction: true,
                    redispatch_details: [{ gen_name: 'G1', voltage_level_id: 'VL1', delta_mw: 10.0, target_mw: 24.3, direction: 'up', current_mw: 14.3, max_raise_mw: 30.0, max_lower_mw: 14.3 }],
                },
            },
            lines_overloaded: [], message: 'done', dc_fallback: false, pdf_path: null, pdf_url: null,
        };

        it('renders an editable input for load-shedding and redispatch rows', () => {
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={injectionList} analysisResult={injectionAnalysis} />);
            expect(screen.getByTestId('explore-mw-load_shedding_L1')).toBeInTheDocument();
            expect(screen.getByTestId('explore-mw-redispatch_G1')).toBeInTheDocument();
        });

        it('defaults the redispatch input to the simulated delta_mw', () => {
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={injectionList} analysisResult={injectionAnalysis} />);
            const input = screen.getByTestId('explore-mw-redispatch_G1') as HTMLInputElement;
            expect(input.value).toBe('10.0');
        });

        it('does not render an editable input for non-injection (disco/reco) rows', () => {
            render(<ExplorePairsTab {...defaultProps} />);
            expect(screen.queryByTestId('explore-mw-act1')).not.toBeInTheDocument();
            expect(screen.queryByTestId('explore-mw-act2')).not.toBeInTheDocument();
        });

        it('passes the edited target MW to onSimulateSingle when the row button is clicked', () => {
            const onSimulateSingle = vi.fn();
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={injectionList} analysisResult={injectionAnalysis} onSimulateSingle={onSimulateSingle} />);
            fireEvent.change(screen.getByTestId('explore-mw-load_shedding_L1'), { target: { value: '12' } });
            // The LS action is untested → button reads "Simulate".
            const row = screen.getByText('load_shedding_L1').closest('tr')!;
            fireEvent.click(within(row).getByText('Simulate'));
            expect(onSimulateSingle).toHaveBeenCalledWith('load_shedding_L1', 12);
        });

        it('passes undefined target MW for a non-injection row (recommender default)', () => {
            const onSimulateSingle = vi.fn();
            render(<ExplorePairsTab {...defaultProps} onSimulateSingle={onSimulateSingle} />);
            // act1 (disco) is pre-simulated in the mock → button reads "Re-run".
            const row = screen.getByText('act1').closest('tr')!;
            fireEvent.click(within(row).getByText('Re-run'));
            expect(onSimulateSingle).toHaveBeenCalledWith('act1', undefined);
        });

        it('renders an editable input for curtailment rows and defaults to curtailed_mw', () => {
            const curtailList = [
                { actionId: 'curtail_G1', score: 3.0, type: 'renewable_curtailment', mwStart: 18.0 },
            ];
            const curtailAnalysis: AnalysisResult = {
                actions: {
                    curtail_G1: {
                        description_unitaire: 'curtail G1', max_rho: 0.7, rho_before: [0.8], rho_after: [0.7],
                        max_rho_line: 'L1', is_rho_reduction: true,
                        curtailment_details: [{ gen_name: 'G1', voltage_level_id: 'VL2', curtailed_mw: 8.5 }],
                    },
                },
                lines_overloaded: [], message: 'done', dc_fallback: false, pdf_path: null, pdf_url: null,
            };
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={curtailList} analysisResult={curtailAnalysis} />);
            const input = screen.getByTestId('explore-mw-curtail_G1') as HTMLInputElement;
            expect(input.value).toBe('8.5');
        });

        it('passes the edited curtailment target MW to onSimulateSingle', () => {
            const onSimulateSingle = vi.fn();
            const curtailList = [
                { actionId: 'curtail_G1', score: 3.0, type: 'renewable_curtailment', mwStart: 18.0 },
            ];
            const curtailAnalysis: AnalysisResult = {
                actions: {},
                lines_overloaded: [], message: 'done', dc_fallback: false, pdf_path: null, pdf_url: null,
            };
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={curtailList} analysisResult={curtailAnalysis} onSimulateSingle={onSimulateSingle} />);
            fireEvent.change(screen.getByTestId('explore-mw-curtail_G1'), { target: { value: '5.5' } });
            const row = screen.getByText('curtail_G1').closest('tr')!;
            fireEvent.click(within(row).getByText('Simulate'));
            expect(onSimulateSingle).toHaveBeenCalledWith('curtail_G1', 5.5);
        });

        it('uses the stored delta_mw when no user edit has been made (redispatch)', () => {
            const onSimulateSingle = vi.fn();
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={injectionList} analysisResult={injectionAnalysis} onSimulateSingle={onSimulateSingle} />);
            const row = screen.getByText('redispatch_G1').closest('tr')!;
            fireEvent.click(within(row).getByText('Re-run'));
            expect(onSimulateSingle).toHaveBeenCalledWith('redispatch_G1', 10.0);
        });

        it('leaves the LS input empty when no simulation result exists yet', () => {
            const lsList = [
                { actionId: 'load_shedding_L1', score: 22, type: 'load_shedding', mwStart: 22.0 },
            ];
            const emptyAnalysis: AnalysisResult = {
                actions: {},
                lines_overloaded: [], message: 'done', dc_fallback: false, pdf_path: null, pdf_url: null,
            };
            render(<ExplorePairsTab {...defaultProps} scoredActionsList={lsList} analysisResult={emptyAnalysis} />);
            const input = screen.getByTestId('explore-mw-load_shedding_L1') as HTMLInputElement;
            expect(input.value).toBe('');
        });
    });
});
