/*
 * SonarQube
 * Copyright (C) 2009-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { times } from 'lodash';
import * as React from 'react';
import { parseDate } from '../../../helpers/dates';
import { mockHistoryItem, mockMeasureHistory } from '../../../helpers/mocks/project-activity';
import { mockMetric } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { byLabelText, byPlaceholderText, byRole, byText } from '../../../helpers/testSelector';
import { ComponentPropsType } from '../../../helpers/testUtils';
import { MetricKey, MetricType } from '../../../types/metrics';
import { GraphType, MeasureHistory } from '../../../types/project-activity';
import { Metric } from '../../../types/types';
import GraphsHeader from '../GraphsHeader';
import GraphsHistory from '../GraphsHistory';
import { generateSeries, getDisplayedHistoryMetrics, splitSeriesInGraphs } from '../utils';

const MAX_GRAPHS = 2;
const MAX_SERIES_PER_GRAPH = 3;
const HISTORY_COUNT = 10;
const START_DATE = '2016-01-01T00:00:00+0200';

describe('rendering', () => {
  it('should render correctly when loading', async () => {
    renderActivityGraph({ loading: true });
    expect(await screen.findByText('loading')).toBeInTheDocument();
  });

  it('should show the correct legend items', async () => {
    const { ui, user } = getPageObject();
    renderActivityGraph();

    // Static legend items, which aren't interactive.
    expect(ui.legendRemoveMetricBtn(MetricKey.violations).query()).not.toBeInTheDocument();
    expect(ui.getLegendItem(MetricKey.violations)).toBeInTheDocument();

    // Switch to custom graph.
    await ui.changeGraphType(GraphType.custom);
    await ui.openAddMetrics();
    await ui.clickOnMetric(MetricKey.bugs);
    await ui.clickOnMetric(MetricKey.test_failures);
    await user.keyboard('{Escape}');

    // These legend items are interactive (interaction tested below).
    expect(ui.legendRemoveMetricBtn(MetricKey.bugs).get()).toBeInTheDocument();
    expect(ui.legendRemoveMetricBtn(MetricKey.test_failures).get()).toBeInTheDocument();

    // Shows warning for metrics with no data.
    const li = ui.getLegendItem(MetricKey.test_failures);
    // eslint-disable-next-line jest/no-conditional-in-test
    if (li) {
      li.focus();
    }
    expect(ui.noDataWarningTooltip.get()).toBeInTheDocument();
  });
});

describe('data table modal', () => {
  it('shows the same data in a table', async () => {
    const { ui } = getPageObject();
    renderActivityGraph();

    await ui.openDataTable();
    expect(ui.dataTable.get()).toBeInTheDocument();
    expect(ui.dataTableColHeaders.getAll()).toHaveLength(3);
    expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);

    // Change graph type and dates, check table updates correctly.
    await ui.closeDataTable();
    await ui.changeGraphType(GraphType.coverage);

    await ui.openDataTable();
    expect(ui.dataTable.get()).toBeInTheDocument();
    expect(ui.dataTableColHeaders.getAll()).toHaveLength(4);
    expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
  });

  it('shows the same data in a table when filtered by date', async () => {
    const { ui } = getPageObject();
    renderActivityGraph({
      graphStartDate: parseDate('2017-01-01'),
      graphEndDate: parseDate('2019-01-01'),
    });

    await ui.openDataTable();
    expect(ui.dataTable.get()).toBeInTheDocument();
    expect(ui.dataTableColHeaders.getAll()).toHaveLength(3);
    expect(ui.dataTableRows.getAll()).toHaveLength(2);
  });
});

it('should correctly handle adding/removing custom metrics', async () => {
  const { ui } = getPageObject();
  renderActivityGraph();

  // Change graph type to "Custom".
  await ui.changeGraphType(GraphType.custom);

  // Open the "Add metrics" dropdown button; select some metrics.
  await ui.openAddMetrics();

  // We should not see DATA type or New Code metrics.
  expect(ui.newBugsCheckbox.query()).not.toBeInTheDocument();
  expect(ui.burnedBudgetCheckbox.query()).not.toBeInTheDocument();

  // Select 3 Int types.
  await ui.clickOnMetric(MetricKey.bugs);
  await ui.clickOnMetric(MetricKey.code_smells);
  await ui.clickOnMetric(MetricKey.confirmed_issues);
  // Select 1 Percent type.
  await ui.clickOnMetric(MetricKey.coverage);

  // We should see 2 graphs, correctly labelled.
  expect(ui.graphs.getAll()).toHaveLength(2);

  // old types and confirmed metrics should be deprecated and show a badge (both in dropdown and in legend)
  expect(ui.deprecatedBadge.getAll()).toHaveLength(6);

  // We cannot select anymore Int types. It should hide options, and show an alert.
  expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument();
  expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument();

  // Select 2 more Percent types.
  await ui.clickOnMetric(MetricKey.duplicated_lines_density);
  await ui.clickOnMetric(MetricKey.test_success_density);

  // We cannot select anymore options. It should disable all remaining options, and
  // show a different alert.
  expect(ui.maxOptionsAlert.get()).toBeInTheDocument();
  expect(ui.vulnerabilityCheckbox.get()).toBeDisabled();

  // Disable a few options.
  await ui.clickOnMetric(MetricKey.bugs);
  await ui.clickOnMetric(MetricKey.code_smells);
  await ui.clickOnMetric(MetricKey.coverage);

  // Search for option.
  await ui.searchForMetric('bug');
  expect(ui.bugsCheckbox.get()).toBeInTheDocument();
  expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument();

  // Disable final metrics by clicking on the legend items.
  await ui.removeMetric(MetricKey.confirmed_issues);
  await ui.removeMetric(MetricKey.duplicated_lines_density);
  await ui.removeMetric(MetricKey.test_success_density);

  // Should show message that there's no data to be rendered.
  expect(ui.noDataText.get()).toBeInTheDocument();
});

function getPageObject() {
  const user = userEvent.setup();
  const ui = {
    // Graph types.
    graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),

    // Add/remove metrics.
    addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
    deprecatedBadge: byText('deprecated'),
    bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }),
    newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }),
    burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }),
    vulnerabilityCheckbox: byRole('checkbox', { name: MetricKey.vulnerabilities }),
    hiddenOptionsAlert: byText('project_activity.graphs.custom.type_x_message', {
      exact: false,
    }),
    maxOptionsAlert: byText('project_activity.graphs.custom.add_metric_info'),
    filterMetrics: byPlaceholderText('search.search_for_metrics'),
    legendRemoveMetricBtn: (key: string) =>
      byRole('button', { name: `project_activity.graphs.custom.remove_metric.${key}` }),
    getLegendItem: (name: string) => {
      // This is due to a limitation in testing library, where we cannot get a listitem
      // role element by name.
      return screen.getAllByRole('listitem').find((item) => item.textContent === name);
    },
    noDataWarningTooltip: byLabelText('project_activity.graphs.custom.metric_no_history'),

    // Graphs.
    graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
    noDataText: byText('project_activity.graphs.custom.no_history'),

    // Data in table.
    openInTableBtn: byRole('button', { name: 'project_activity.graphs.open_in_table' }),
    closeDataTableBtn: byRole('button', { name: 'close' }),
    dataTable: byRole('table'),
    dataTableRows: byRole('row'),
    dataTableColHeaders: byRole('columnheader'),
    noDataTableText: byText('project_activity.graphs.data_table.no_data_warning_check_dates_x', {
      exact: false,
    }),
  };

  return {
    user,
    ui: {
      ...ui,
      async changeGraphType(type: GraphType) {
        await user.click(ui.graphTypeSelect.get());
        const optionForType = await screen.findByText(`project_activity.graphs.${type}`);
        await user.click(optionForType);
      },
      async openAddMetrics() {
        await user.click(ui.addMetricBtn.get());
      },
      async searchForMetric(text: string) {
        await user.type(ui.filterMetrics.get(), text);
      },
      async clickOnMetric(name: MetricKey) {
        await user.click(screen.getByRole('checkbox', { name }));
      },
      async removeMetric(metric: MetricKey) {
        await user.click(ui.legendRemoveMetricBtn(metric).get());
      },
      async openDataTable() {
        await user.click(ui.openInTableBtn.get());
      },
      async closeDataTable() {
        await user.click(ui.closeDataTableBtn.get());
      },
    },
  };
}

function renderActivityGraph(
  graphsHistoryProps: Partial<GraphsHistory['props']> = {},
  graphsHeaderProps: Partial<ComponentPropsType<typeof GraphsHeader>> = {},
) {
  function ActivityGraph() {
    const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]);
    const [graph, setGraph] = React.useState(graphsHistoryProps.graph || GraphType.issues);

    const measuresHistory: MeasureHistory[] = [];
    const metrics: Metric[] = [];
    [
      MetricKey.violations,
      MetricKey.bugs,
      MetricKey.code_smells,
      MetricKey.confirmed_issues,
      MetricKey.vulnerabilities,
      MetricKey.blocker_violations,
      MetricKey.lines_to_cover,
      MetricKey.uncovered_lines,
      MetricKey.coverage,
      MetricKey.duplicated_lines_density,
      MetricKey.test_success_density,
    ].forEach((metric) => {
      const history = times(HISTORY_COUNT - 2, (i) => {
        const date = parseDate(START_DATE);
        date.setDate(date.getDate() + i);
        return mockHistoryItem({ date, value: i.toString() });
      });
      history.push(
        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200') }),
        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200') }),
      );
      measuresHistory.push(mockMeasureHistory({ metric, history }));
      metrics.push(
        mockMetric({
          key: metric,
          type:
            metric.includes('_density') || metric === MetricKey.coverage
              ? MetricType.Percent
              : MetricType.Integer,
        }),
      );
    });

    // The following should be filtered out, and not be suggested as options.
    metrics.push(
      mockMetric({ key: MetricKey.new_bugs, type: MetricType.Integer }),
      mockMetric({ key: MetricKey.burned_budget, type: MetricType.Data }),
    );

    // The following will not be filtered out, but has no values.
    metrics.push(mockMetric({ key: MetricKey.test_failures, type: MetricType.Integer }));
    measuresHistory.push(
      mockMeasureHistory({
        metric: MetricKey.test_failures,
        history: times(HISTORY_COUNT, (i) => {
          const date = parseDate(START_DATE);
          date.setDate(date.getDate() + i);
          return mockHistoryItem({ date, value: undefined });
        }),
      }),
    );

    const series = generateSeries(
      measuresHistory,
      graph,
      metrics,
      getDisplayedHistoryMetrics(graph, selectedMetrics),
    );
    const graphs = splitSeriesInGraphs(series, MAX_GRAPHS, MAX_SERIES_PER_GRAPH);
    const metricsTypeFilter =
      graphs.length < MAX_GRAPHS
        ? undefined
        : graphs
            .filter((graph) => graph.length < MAX_SERIES_PER_GRAPH)
            .map((graph) => graph[0].type);

    const addCustomMetric = (metricKey: string) => {
      setSelectedMetrics([...selectedMetrics, metricKey]);
    };

    const removeCustomMetric = (metricKey: string) => {
      setSelectedMetrics(selectedMetrics.filter((m) => m !== metricKey));
    };

    const updateGraph = (graphType: string) => {
      setGraph(graphType as GraphType);
    };

    return (
      <>
        <GraphsHeader
          onAddCustomMetric={addCustomMetric}
          graph={graph}
          metrics={metrics}
          metricsTypeFilter={metricsTypeFilter}
          onRemoveCustomMetric={removeCustomMetric}
          selectedMetrics={selectedMetrics}
          onUpdateGraph={updateGraph}
          {...graphsHeaderProps}
        />
        <GraphsHistory
          analyses={[]}
          graph={graph}
          graphs={graphs}
          loading={false}
          measuresHistory={[]}
          removeCustomMetric={removeCustomMetric}
          series={series}
          {...graphsHistoryProps}
        />
      </>
    );
  }

  return renderComponent(<ActivityGraph />);
}
