import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { cloneDeep } from 'lodash';
import { useParams } from 'react-router-dom-v5-compat';
import { of } from 'rxjs';
import { TestProvider } from 'test/helpers/TestProvider';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';

import { type PanelProps, store, systemDateFormats, type SystemDateFormatsState } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { selectors } from '@grafana/e2e-selectors';
import {
  LocationServiceProvider,
  locationSearchToObject,
  locationService,
  setPluginImportUtils,
} from '@grafana/runtime';
import { setGetObservablePluginLinks, setPanelPluginMetas } from '@grafana/runtime/internal';
import { VizPanel } from '@grafana/scenes';
import { type Dashboard } from '@grafana/schema';
import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps';
import { type GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { type DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types/dashboard';

import { setPublicDashboardConfigFn } from '../../dashboard/components/PublicDashboard/usePublicDashboardConfig';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { setupLoadDashboardMockReject, setupLoadDashboardRuntimeErrorMock } from '../utils/test-utils';

import { DashboardScenePage, type Props } from './DashboardScenePage';
import {
  DashboardScenePageStateManager,
  DashboardScenePageStateManagerV2,
  getDashboardScenePageStateManager,
} from './DashboardScenePageStateManager';

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  setPluginExtensionGetter: jest.fn(),
  useChromeHeaderHeight: jest.fn().mockReturnValue(80),
  getBackendSrv: () => {
    return {
      get: jest.fn().mockResolvedValue({ dashboard: simpleDashboard, meta: { url: '' } }),
    };
  },
  getDataSourceSrv: () => {
    return {
      get: jest.fn().mockResolvedValue({}),
      getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
    };
  },
  getAppEvents: () => ({
    publish: jest.fn(),
  }),
}));

jest.mock('react-router-dom-v5-compat', () => ({
  ...jest.requireActual('react-router-dom-v5-compat'),
  useParams: jest.fn().mockReturnValue({ uid: 'my-dash-uid' }),
}));

const getObservablePluginLinks = jest.fn().mockReturnValue(of([]));
setGetObservablePluginLinks(getObservablePluginLinks);

function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps> } = {}) {
  const context = getGrafanaContextMock();
  const defaultRouteProps = getRouteComponentProps();
  const props: Props = {
    ...defaultRouteProps,
    ...routeProps,
  };

  const renderResult = render(
    <TestProvider grafanaContext={context}>
      <LocationServiceProvider service={locationService}>
        <DashboardScenePage {...props} />
      </LocationServiceProvider>
    </TestProvider>
  );

  const rerender = (newProps: Props) => {
    renderResult.rerender(
      <TestProvider grafanaContext={context}>
        <LocationServiceProvider service={locationService}>
          <DashboardScenePage {...newProps} />
        </LocationServiceProvider>
      </TestProvider>
    );
  };

  return { rerender, context, props, unmount: renderResult.unmount };
}

const simpleDashboard: Dashboard = {
  title: 'My cool dashboard',
  uid: 'my-dash-uid',
  schemaVersion: 30,
  version: 1,
  panels: [
    {
      id: 1,
      type: 'custom-viz-panel',
      title: 'Panel A',
      options: {
        content: `Content A`,
      },
      gridPos: {
        x: 0,
        y: 0,
        w: 10,
        h: 10,
      },
      targets: [],
    },
    {
      id: 2,
      type: 'custom-viz-panel',
      title: 'Panel B',
      options: {
        content: `Content B`,
      },
      gridPos: {
        x: 0,
        y: 10,
        w: 10,
        h: 10,
      },
      targets: [],
    },
  ],
};

const panelPlugin = getPanelPlugin(
  {
    skipDataQuery: true,
  },
  CustomVizPanel
);

beforeEach(() => {
  setPanelPluginMetas({ 'custom-viz-panel': panelPlugin.meta });
});

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

setPluginImportUtils({
  importPanelPlugin: (id: string) => Promise.resolve(panelPlugin),
  getPanelPluginFromCache: (id: string) => undefined,
});

const loadDashboardMock = jest.fn();

setDashboardLoaderSrv({
  loadDashboard: loadDashboardMock,
  // disabling type checks since this is a test util
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
} as unknown as DashboardLoaderSrv);

describe('DashboardScenePage', () => {
  beforeEach(() => {
    setPublicDashboardConfigFn({
      footerHide: false,
      footerText: 'Powered by',
      footerLogo: 'grafana-logo',
      footerLink: 'https://grafana.com/?src=grafananet&cnt=public-dashboards',
      headerLogoHide: false,
    });
    locationService.push('/d/my-dash-uid');
    getDashboardScenePageStateManager().clearDashboardCache();
    loadDashboardMock.mockClear();
    loadDashboardMock.mockResolvedValue({ dashboard: simpleDashboard, meta: { slug: '123' } });
    // hacky way because mocking autosizer does not work
    Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
    Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
    getObservablePluginLinks.mockRestore();
    getObservablePluginLinks.mockReturnValue(of([]));
    store.delete(DASHBOARD_FROM_LS_KEY);
  });

  it('Can render dashboard', async () => {
    setup();

    await waitForDashboardToRender();

    expect(await screen.findByTitle('Panel A')).toBeInTheDocument();
    expect(await screen.findByText('Content A')).toBeInTheDocument();

    expect(await screen.findByTitle('Panel B')).toBeInTheDocument();
    expect(await screen.findByText('Content B')).toBeInTheDocument();
  });

  it('shows Powered by footer in kiosk mode', async () => {
    setup({ routeProps: { queryParams: { kiosk: true } } });

    await waitForDashboardToRender();

    expect(await screen.findByTestId(selectors.pages.PublicDashboard.footer)).toBeInTheDocument();
  });

  it('shows kiosk Powered by footer even when public dashboard footerHide is enabled', async () => {
    setPublicDashboardConfigFn({
      footerHide: true,
      footerText: 'Powered by',
      footerLogo: 'grafana-logo',
      footerLink: 'https://grafana.com/?src=grafananet&cnt=public-dashboards',
      headerLogoHide: false,
    });

    setup({ routeProps: { queryParams: { kiosk: true } } });

    await waitForDashboardToRender();

    expect(await screen.findByTestId(selectors.pages.PublicDashboard.footer)).toBeInTheDocument();
  });

  it('shows Powered by footer when kiosk query param is present with no value (?kiosk)', async () => {
    setup({ routeProps: { queryParams: locationSearchToObject('?kiosk') } });

    await waitForDashboardToRender();

    expect(await screen.findByTestId(selectors.pages.PublicDashboard.footer)).toBeInTheDocument();
  });

  it('does not show Powered by footer when kiosk=false', async () => {
    setup({ routeProps: { queryParams: { kiosk: 'false' } } });

    await waitForDashboardToRender();

    expect(screen.queryByTestId(selectors.pages.PublicDashboard.footer)).not.toBeInTheDocument();
  });

  it('uses kiosk dashboard CTA url', async () => {
    setup({ routeProps: { queryParams: { kiosk: true } } });

    await waitForDashboardToRender();

    const footer = await screen.findByTestId(selectors.pages.PublicDashboard.footer);
    const link = footer.querySelector('a');
    expect(link).toHaveAttribute('href', 'https://grafana.com/?src=grafananet&cnt=kiosk-dashboard');
  });

  it('hides Powered by footer in kiosk mode when hideLogo is present', async () => {
    setup({ routeProps: { queryParams: { kiosk: true, hideLogo: true } } });

    await waitForDashboardToRender();

    expect(screen.queryByTestId(selectors.pages.PublicDashboard.footer)).not.toBeInTheDocument();
  });

  it('routeReloadCounter should trigger reload', async () => {
    const { rerender, props } = setup();

    await waitForDashboardToRender();

    expect(await screen.findByTitle('Panel A')).toBeInTheDocument();

    const updatedDashboard = cloneDeep(simpleDashboard);
    updatedDashboard.version = 11;
    updatedDashboard.panels![0].title = 'Updated title';

    getDashboardScenePageStateManager().clearDashboardCache();
    loadDashboardMock.mockResolvedValue({ dashboard: updatedDashboard, meta: {} });

    props.location.state = { routeReloadCounter: 1 };

    rerender(props);

    expect(await screen.findByTitle('Updated title')).toBeInTheDocument();
  });

  it('Can inspect panel', async () => {
    setup();

    await waitForDashboardToRender();

    expect(screen.queryByText('Inspect: Panel B')).not.toBeInTheDocument();

    // Wish I could use the menu here but unable t get it to open when I click the menu button
    // Somethig with Dropdown that is not working inside react-testing
    await userEvent.click(screen.getByLabelText('Menu for panel Panel B'));

    const inspectMenuItem = await screen.findAllByText('Inspect');

    await userEvent.click(inspectMenuItem[0]);

    expect(await screen.findByText('Inspect: Panel B')).toBeInTheDocument();

    await userEvent.click(screen.getByTestId(selectors.components.Drawer.General.close));

    expect(screen.queryByText('Inspect: Panel B')).not.toBeInTheDocument();
  });

  it('Can view panel in fullscreen', async () => {
    setup();

    await waitForDashboardToRender();

    expect(await screen.findByTitle('Panel A')).toBeInTheDocument();

    act(() => locationService.partial({ viewPanel: '2' }));

    expect(screen.queryByTitle('Panel A')).not.toBeInTheDocument();
    expect(await screen.findByTitle('Panel B')).toBeInTheDocument();
  });

  describe('absolute time range', () => {
    it('should render with absolute time range when use_browser_locale is true', async () => {
      locationService.push('/d/my-dash-uid?from=2025-03-11T07:09:37.253Z&to=2025-03-12T07:09:37.253Z');
      systemDateFormats.update({
        fullDate: 'YYYY-MM-DD HH:mm:ss.SSS',
        interval: {} as SystemDateFormatsState['interval'],
        useBrowserLocale: true,
      });
      setup();

      await waitForDashboardToRenderWithTimeRange({
        from: '03/11/2025, 02:09:37 AM',
        to: '03/12/2025, 02:09:37 AM',
      });
    });

    it('should render correct time range when use_browser_locale is true and time range is other than default system date format', async () => {
      locationService.push('/d/my-dash-uid?from=2025-03-11T07:09:37.253Z&to=2025-03-12T07:09:37.253Z');
      // mocking navigator.languages to return 'de'
      // this property configured in the browser settings
      Object.defineProperty(navigator, 'languages', { value: ['de'] });
      systemDateFormats.update({
        // left fullDate empty to show that this should be overridden by the browser locale
        fullDate: '',
        interval: {} as SystemDateFormatsState['interval'],
        useBrowserLocale: true,
      });
      setup();

      await waitForDashboardToRenderWithTimeRange({
        from: '11.03.2025, 02:09:37',
        to: '12.03.2025, 02:09:37',
      });
    });
  });

  describe('empty state', () => {
    it('Shows empty state when dashboard is empty', async () => {
      loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} });
      setup();

      expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument();
    });

    it('shows and hides empty state when panels are added and removed', async () => {
      setup();

      await waitForDashboardToRender();

      expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();

      // Hacking a bit, accessing private cache property to get access to the underlying DashboardScene object
      const dashboardScenesCache = getDashboardScenePageStateManager().getCache();
      const dashboard = dashboardScenesCache['my-dash-uid'];
      const panels = dashboardSceneGraph.getVizPanels(dashboard);

      act(() => {
        dashboard.removePanel(panels[0]);
      });
      expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();

      act(() => {
        dashboard.removePanel(panels[1]);
      });
      expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument();

      act(() => {
        dashboard.addPanel(new VizPanel({ title: 'Panel Added', key: 'panel-4', pluginId: 'timeseries' }));
      });

      expect(await screen.findByTitle('Panel Added')).toBeInTheDocument();
      expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
    });
  });

  describe('home page', () => {
    it('should render the dashboard when the route is home', async () => {
      (useParams as jest.Mock).mockReturnValue({});
      setup({
        routeProps: {
          route: {
            ...getRouteComponentProps().route,
            routeName: DashboardRoutes.Home,
          },
        },
      });

      await waitForDashboardToRender();

      expect(await screen.findByTitle('Panel A')).toBeInTheDocument();
      expect(await screen.findByText('Content A')).toBeInTheDocument();

      expect(await screen.findByTitle('Panel B')).toBeInTheDocument();
      expect(await screen.findByText('Content B')).toBeInTheDocument();
    });

    it('should show controls', async () => {
      getDashboardScenePageStateManager().clearDashboardCache();
      loadDashboardMock.mockClear();
      loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} });

      setup();

      await waitFor(() => expect(screen.queryByText('Refresh')).toBeInTheDocument());
      await waitFor(() => expect(screen.queryByText('Last 6 hours')).toBeInTheDocument());
    });
  });

  describe('errors rendering', () => {
    const origError = console.error;
    const consoleErrorMock = jest.fn();
    afterEach(() => (console.error = origError));
    beforeEach(() => (console.error = consoleErrorMock));

    it('should render dashboard not found notice when dashboard... not found', async () => {
      setupLoadDashboardMockReject({
        status: 404,
        statusText: 'Not Found',
        data: {
          message: 'Dashboard not found',
        },
        config: {
          method: 'GET',
          url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
          retry: 0,
          headers: {
            'X-Grafana-Org-Id': 1,
          },
          hideFromInspector: true,
        },
        isHandled: true,
      });

      setup();

      expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
    });

    it('should render error alert for backend errors', async () => {
      setupLoadDashboardMockReject({
        status: 500,
        statusText: 'internal server error',
        data: {
          message: 'Internal server error',
        },
        config: {
          method: 'GET',
          url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
          retry: 0,
          headers: {
            'X-Grafana-Org-Id': 1,
          },
          hideFromInspector: true,
        },
        isHandled: true,
      });

      setup();

      expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
      expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Internal server error');
    });

    it('should render error alert for runtime errors', async () => {
      setupLoadDashboardRuntimeErrorMock();

      setup();

      expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
      expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Runtime error');
    });
  });

  describe('UnifiedDashboardScenePageStateManager', () => {
    it('should reset active manager when unmounting', async () => {
      const manager = getDashboardScenePageStateManager();
      manager.setActiveManager('v2');

      // This test is only validating manager cleanup on unmount.
      // Prevent the component from triggering a real v2 dashboard load (which requires extra setup).
      const loadDashboardSpy = jest.spyOn(manager, 'loadDashboard').mockResolvedValue(undefined);

      try {
        const { unmount } = setup();

        expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
        unmount();
        expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager);
      } finally {
        loadDashboardSpy.mockRestore();
      }
    });
  });
});

interface VizOptions {
  content: string;
}
interface VizProps extends PanelProps<VizOptions> {}

function CustomVizPanel(props: VizProps) {
  return <div>{props.options.content}</div>;
}

async function waitForDashboardToRender() {
  expect(await screen.findByText('Last 6 hours')).toBeInTheDocument();
  expect(await screen.findByTitle('Panel A')).toBeInTheDocument();
}

async function waitForDashboardToRenderWithTimeRange(timeRange: { from: string; to: string }) {
  expect(await screen.findByText(`${timeRange.from} to ${timeRange.to}`)).toBeInTheDocument();
  expect(await screen.findByTitle('Panel A')).toBeInTheDocument();
}
