/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

// Mock isMatrixifyEnabled before loading any modules
import fetchMock from 'fetch-mock';
import {
  getChartControlPanelRegistry,
  getChartMetadataRegistry,
  ChartMetadata,
  VizType,
} from '@superset-ui/core';
import { QUERY_MODE_REQUISITES } from 'src/explore/constants';
import { MemoryRouter, Route } from 'react-router-dom';
import {
  render,
  screen,
  userEvent,
  waitFor,
} from 'spec/helpers/testing-library';
import ExploreViewContainer from '.';

jest.doMock('@superset-ui/core', () => ({
  __esModule: true,
  ...jest.requireActual('@superset-ui/core'),
  isMatrixifyEnabled: jest.fn(() => false),
}));

const reduxState = {
  explore: {
    controls: {
      datasource: { value: '1__table' },
      viz_type: { value: VizType.Table },
    },
    datasource: {
      id: 1,
      type: 'table',
      columns: [{ is_dttm: false }],
      metrics: [{ id: 1, metric_name: 'count' }],
    },
    isStarred: false,
    slice: {
      slice_id: 1,
    },
    metadata: {
      created_on_humanized: 'a week ago',
      changed_on_humanized: '2 days ago',
      owners: ['John Doe'],
      created_by: 'John Doe',
      changed_by: 'John Doe',
      dashboards: [{ id: 1, dashboard_title: 'Test' }],
    },
  },
  charts: {
    1: {
      id: 1,
      latestQueryFormData: {
        datasource: '1__table',
      },
    },
  },
  user: {
    userId: 1,
  },
  common: { conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 } },
  datasources: {
    '1__table': {
      id: 1,
      type: 'table',
      columns: [{ is_dttm: false }],
      metrics: [{ id: 1, metric_name: 'count' }],
    },
  },
};

const KEY = 'aWrs7w29sd';
const SEARCH = `?form_data_key=${KEY}&dataset_id=1`;

jest.mock(
  'src/explore/components/ExploreChartPanel/useResizeDetectorByObserver',
  () => ({
    __esModule: true,
    default: () => ({ height: 100, width: 100 }),
  }),
);

jest.mock('lodash/debounce', () => ({
  __esModule: true,
  default: (fuc: Function) => fuc,
}));

fetchMock.post('glob:*/api/v1/explore/form_data*', { key: KEY });
fetchMock.put('glob:*/api/v1/explore/form_data*', { key: KEY });
fetchMock.get('glob:*/api/v1/explore/form_data*', {});
fetchMock.get('glob:*/api/v1/chart/favorite_status*', {
  result: [{ value: true }],
});
fetchMock.get('glob:*/api/v1/chart/*', {
  result: {},
});

const defaultPath = '/explore/';
const renderWithRouter = ({
  search = '',
  overridePathname,
  initialState = reduxState,
}: {
  search?: string;
  overridePathname?: string;
  initialState?: object;
} = {}) => {
  const path = overridePathname ?? defaultPath;
  Object.defineProperty(window, 'location', {
    get() {
      return { pathname: path, search };
    },
  });
  return render(
    <MemoryRouter initialEntries={[`${path}${search}`]}>
      <Route path={path}>
        <ExploreViewContainer />
      </Route>
    </MemoryRouter>,
    { useRedux: true, useDnd: true, initialState },
  );
};

test('generates a new form_data param when none is available', async () => {
  getChartMetadataRegistry().registerValue(
    'table',
    new ChartMetadata({
      name: 'fake table',
      thumbnail: '.png',
      useLegacyApi: false,
    }),
  );
  const replaceState = jest.spyOn(window.history, 'replaceState');
  await waitFor(() => renderWithRouter());
  expect(replaceState).toHaveBeenCalledWith(
    expect.anything(),
    undefined,
    expect.stringMatching('form_data_key'),
  );
  expect(replaceState).toHaveBeenCalledWith(
    expect.anything(),
    undefined,
    expect.stringMatching('datasource_id'),
  );
  replaceState.mockRestore();
});

test('renders chart in standalone mode', () => {
  const { queryByTestId } = renderWithRouter({
    initialState: {
      ...reduxState,
      explore: { ...reduxState.explore, standalone: true },
    },
  });
  expect(queryByTestId('standalone-app')).toBeInTheDocument();
});

test('generates a different form_data param when one is provided and is mounting', async () => {
  const replaceState = jest.spyOn(window.history, 'replaceState');
  await waitFor(() => renderWithRouter({ search: SEARCH }));
  expect(replaceState).not.toHaveBeenLastCalledWith(
    0,
    expect.anything(),
    undefined,
    expect.stringMatching(KEY),
  );
  expect(replaceState).toHaveBeenCalledWith(
    expect.anything(),
    undefined,
    expect.stringMatching('datasource_id'),
  );
  replaceState.mockRestore();
});

test('reuses the same form_data param when updating', async () => {
  getChartControlPanelRegistry().registerValue('table', {
    controlPanelSections: [],
  });
  const replaceState = jest.spyOn(window.history, 'replaceState');
  const pushState = jest.spyOn(window.history, 'pushState');
  await waitFor(() => renderWithRouter({ search: SEARCH }));
  expect(replaceState.mock.calls.length).toBe(1);
  userEvent.click(screen.getByText('Update chart'));
  await waitFor(() => expect(pushState.mock.calls.length).toBe(1));
  expect(replaceState.mock.calls[0]).toEqual(pushState.mock.calls[0]);
  replaceState.mockRestore();
  pushState.mockRestore();
  getChartControlPanelRegistry().remove('table');
});

test('doesnt call replaceState when pathname is not /explore', async () => {
  getChartMetadataRegistry().registerValue(
    'table',
    new ChartMetadata({
      name: 'fake table',
      thumbnail: '.png',
      useLegacyApi: false,
    }),
  );
  const replaceState = jest.spyOn(window.history, 'replaceState');
  await waitFor(() => renderWithRouter({ overridePathname: '/dashboard' }));
  expect(replaceState).not.toHaveBeenCalled();
  replaceState.mockRestore();
});

test('preserves unknown parameters', async () => {
  const replaceState = jest.spyOn(window.history, 'replaceState');
  const unknownParam = 'test=123';
  await waitFor(() =>
    renderWithRouter({ search: `${SEARCH}&${unknownParam}` }),
  );
  expect(replaceState).toHaveBeenCalledWith(
    expect.anything(),
    undefined,
    expect.stringMatching(unknownParam),
  );
  replaceState.mockRestore();
});

test('retains query mode requirements when query_mode is enabled', async () => {
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      controls: {
        ...reduxState.explore.controls,
        query_mode: { value: 'raw' },
        optional_key1: { value: 'value1' },
        all_columns: { value: ['all_columns'] },
        groupby: { value: ['groupby'] },
      },
      hiddenFormData: {
        all_columns: ['all_columns'],
        groupby: ['groupby'],
        optional_key1: 'value1',
      },
    },
  };

  await waitFor(() => renderWithRouter({ initialState: customState }));

  const formDataEndpointCalls = fetchMock.calls(/api\/v1\/explore\/form_data/);
  expect(formDataEndpointCalls.length).toBeGreaterThan(0);
  const lastCall = formDataEndpointCalls[formDataEndpointCalls.length - 1];

  const body = JSON.parse(lastCall[1]?.body as string);
  const formData = JSON.parse(body.form_data);

  const queryModeFields = Object.keys(
    customState.explore.hiddenFormData,
  ).filter(key => QUERY_MODE_REQUISITES.has(key));

  queryModeFields.forEach(key => {
    expect(formData[key]).toBeDefined();
  });
  expect(formData.optional_key1).toBeUndefined();
});

test('does omit hiddenFormData when query_mode is not enabled', async () => {
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      controls: {
        ...reduxState.explore.controls,
        optional_key1: { value: 'value1' },
        all_columns: { value: ['all_columns'] },
        groupby: { value: ['groupby'] },
      },
      hiddenFormData: {
        all_columns: ['all_columns'],
        groupby: ['groupby'],
        optional_key1: 'value1',
      },
    },
  };

  await waitFor(() => renderWithRouter({ initialState: customState }));

  const formDataEndpointCalls = fetchMock.calls(/api\/v1\/explore\/form_data/);
  expect(formDataEndpointCalls.length).toBeGreaterThan(0);
  const lastCall = formDataEndpointCalls[formDataEndpointCalls.length - 1];

  const body = JSON.parse(lastCall[1]?.body as string);
  const formData = JSON.parse(body.form_data);

  Object.keys(customState.explore.hiddenFormData).forEach(key => {
    expect(formData[key]).toBeUndefined();
  });
});

// Component tests for the errorMessage behavior
test('does not show error indicator when no controls have validation errors', async () => {
  getChartControlPanelRegistry().registerValue('table', {
    controlPanelSections: [],
  });
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      controls: {
        ...reduxState.explore.controls,
        metric: { value: 'count', label: 'Metric' },
        groupby: { value: ['category'], label: 'Group by' },
      },
    },
  };

  renderWithRouter({ initialState: customState });

  await waitFor(() => {
    const errorIndicator = screen.queryByTestId('query-error-tooltip-trigger');
    expect(errorIndicator).not.toBeInTheDocument();
  });
});

test('shows error indicator when controls have validation errors', async () => {
  getChartControlPanelRegistry().registerValue('table', {
    controlPanelSections: [],
  });
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      controls: {
        ...reduxState.explore.controls,
        metric: {
          value: '',
          label: 'Metric',
          validationErrors: ['Metric is required'],
        },
      },
    },
  };

  renderWithRouter({ initialState: customState });

  const errorIndicator = await screen.findByTestId(
    'query-error-tooltip-trigger',
  );

  userEvent.hover(errorIndicator);

  const tooltip = await screen.findByRole('tooltip');
  expect(tooltip).toBeInTheDocument();

  const errorMessage = await screen.findByText(/Metric is required/);
  expect(errorMessage).toBeInTheDocument();
});

test('shows error indicator for multiple controls with validation errors', async () => {
  getChartControlPanelRegistry().registerValue('table', {
    controlPanelSections: [],
  });
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      controls: {
        ...reduxState.explore.controls,
        metric: {
          value: '',
          label: 'Metric',
          validationErrors: ['Field is required'],
        },
        groupby: {
          value: [],
          label: 'Group by',
          validationErrors: ['Field is required'],
        },
      },
    },
  };

  renderWithRouter({ initialState: customState });

  const errorIndicator = await screen.findByTestId(
    'query-error-tooltip-trigger',
  );

  userEvent.hover(errorIndicator);

  const tooltip = await screen.findByRole('tooltip');
  expect(tooltip).toBeInTheDocument();

  expect(await screen.findByText(/Field is required/)).toBeInTheDocument();
});

test('shows error indicator for control with multiple validation errors', async () => {
  getChartControlPanelRegistry().registerValue('table', {
    controlPanelSections: [],
  });
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      controls: {
        ...reduxState.explore.controls,
        metric: {
          value: '',
          label: 'Metric',
          validationErrors: ['Field is required', 'Invalid format'],
        },
      },
    },
  };

  renderWithRouter({ initialState: customState });

  const errorIndicator = await screen.findByTestId(
    'query-error-tooltip-trigger',
  );

  userEvent.hover(errorIndicator);

  const tooltip = await screen.findByRole('tooltip');
  expect(tooltip).toBeInTheDocument();

  expect(await screen.findByText(/Field is required/)).toBeInTheDocument();
  expect(await screen.findByText(/Invalid format/)).toBeInTheDocument();
});

test('shows error indicator with function labels', async () => {
  getChartControlPanelRegistry().registerValue('table', {
    controlPanelSections: [],
  });
  // Ensure the explore state passed to the label function contains the expected property
  const customState = {
    ...reduxState,
    explore: {
      ...reduxState.explore,
      someState: 'test',
      controls: {
        ...reduxState.explore.controls,
        metric: {
          value: '',
          label: (exploreState: { someState: string }) =>
            `Dynamic Metric (${exploreState.someState})`,
          validationErrors: ['Metric is required'],
        },
      },
    },
  };

  renderWithRouter({ initialState: customState });

  const errorIndicator = await screen.findByTestId(
    'query-error-tooltip-trigger',
  );

  userEvent.hover(errorIndicator);

  const tooltip = await screen.findByRole('tooltip');
  expect(tooltip).toBeInTheDocument();

  expect(await screen.findByText(/Metric is required/)).toBeInTheDocument();
});
