import {
  type SceneGridItemLike,
  SceneGridLayout,
  SceneGridRow,
  SceneQueryRunner,
  SceneTimeRange,
  SceneVariableSet,
  VizPanel,
} from '@grafana/scenes';

import { findVizPanelByKey, getQueryRunnerFor } from '../../utils/utils';
import { DashboardScene, type DashboardSceneState } from '../DashboardScene';
import { AutoGridItem } from '../layout-auto-grid/AutoGridItem';
import { AutoGridLayout } from '../layout-auto-grid/AutoGridLayout';
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';

import { DashboardGridItem } from './DashboardGridItem';
import { DefaultGridLayoutManager } from './DefaultGridLayoutManager';

describe('DefaultGridLayoutManager', () => {
  describe('getVizPanels', () => {
    it('Should return all panels', () => {
      const { manager } = setup();
      const vizPanels = manager.getVizPanels();

      expect(vizPanels.length).toBe(4);
      expect(vizPanels[0].state.title).toBe('Panel A');
      expect(vizPanels[1].state.title).toBe('Panel B');
      expect(vizPanels[2].state.title).toBe('Panel C');
      expect(vizPanels[3].state.title).toBe('Panel D');
    });

    it('Should return an empty array when scene has no panels', () => {
      const { manager } = setup({ gridItems: [] });
      const vizPanels = manager.getVizPanels();
      expect(vizPanels.length).toBe(0);
    });
  });

  describe('addPanel', () => {
    it('Should add a new panel', () => {
      const { manager } = setup();

      const vizPanel = new VizPanel({
        title: 'Panel Title',
        pluginId: 'timeseries',
        $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
      });

      manager.addPanel(vizPanel);

      const panel = findVizPanelByKey(manager, vizPanel.state.key)!;
      const gridItem = panel.parent as DashboardGridItem;

      expect(panel).toBeDefined();
      expect(gridItem.state.y).toBe(0);
    });
  });

  describe('addNewRow', () => {
    it('Should create and add a new row to the dashboard', () => {
      const { manager, grid } = setup();
      const row = manager.addNewRow();

      expect(grid.state.children.length).toBe(2);
      expect(row.state.key).toBe('panel-4');
      expect(row.state.children[0].state.key).toBe('griditem-1');
      expect(row.state.children[1].state.key).toBe('griditem-2');
    });

    it('Should create a row and add all panels in the dashboard under it', () => {
      const { manager, grid } = setup({
        gridItems: [
          new DashboardGridItem({
            key: 'griditem-1',
            x: 0,
            body: new VizPanel({
              title: 'Panel A',
              key: 'panel-1',
              pluginId: 'table',
              $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
            }),
          }),
          new DashboardGridItem({
            key: 'griditem-2',
            body: new VizPanel({
              title: 'Panel B',
              key: 'panel-2',
              pluginId: 'table',
            }),
          }),
        ],
      });

      const row = manager.addNewRow();

      expect(grid.state.children.length).toBe(1);
      expect(row.state.children.length).toBe(2);
    });

    it('Should create and add two new rows, but the second has no children', () => {
      const { manager, grid } = setup();
      const row1 = manager.addNewRow();
      const row2 = manager.addNewRow();

      expect(grid.state.children.length).toBe(3);
      expect(row1.state.children.length).toBe(2);
      expect(row2.state.children.length).toBe(0);
    });

    it('Should create an empty row when nothing else in dashboard', () => {
      const { manager, grid } = setup({ gridItems: [] });
      const row = manager.addNewRow();

      expect(grid.state.children.length).toBe(1);
      expect(row.state.children.length).toBe(0);
    });
  });

  describe('Remove row', () => {
    it('Should remove a row and move its children to the grid layout', () => {
      const { manager, grid } = setup();
      const row = grid.state.children[2] as SceneGridRow;

      manager.removeRow(row);

      expect(grid.state.children.length).toBe(4);
    });

    it('Should remove a row and its children', () => {
      const { manager, grid } = setup();
      const row = grid.state.children[2] as SceneGridRow;

      manager.removeRow(row, true);

      expect(grid.state.children.length).toBe(2);
    });

    it('Should remove an empty row from the layout', () => {
      const row = new SceneGridRow({ key: 'panel-1' });
      const { manager, grid } = setup({ gridItems: [row] });

      manager.removeRow(row);

      expect(grid.state.children.length).toBe(0);
    });
  });

  describe('removePanel', () => {
    it('Should remove grid item', () => {
      const { manager } = setup();
      const panel = findVizPanelByKey(manager, 'panel-1')!;
      manager.removePanel(panel);

      expect(findVizPanelByKey(manager, 'panel-1')).toBeNull();
    });

    it('Should remove a grid item within a row', () => {
      const { manager, grid } = setup();
      const vizPanel = findVizPanelByKey(manager, 'panel-within-row1')!;

      manager.removePanel(vizPanel);

      const gridRow = grid.state.children[2] as SceneGridRow;
      expect(gridRow.state.children.length).toBe(1);
    });
  });

  describe('duplicatePanel', () => {
    it('Should duplicate a panel', () => {
      const { manager, grid } = setup();
      const vizPanel = findVizPanelByKey(manager, 'panel-1')!;

      expect(grid.state.children.length).toBe(3);

      manager.duplicatePanel(vizPanel);

      const newGridItem = grid.state.children[3];

      expect(grid.state.children.length).toBe(4);
      expect(newGridItem.state.key).toBe('grid-item-4');
    });

    it('Should maintain size of duplicated panel', () => {
      const { manager, grid } = setup();

      const gItem = grid.state.children[0] as DashboardGridItem;
      gItem.setState({ height: 1 });

      const vizPanel = gItem.state.body;
      manager.duplicatePanel(vizPanel);

      const newGridItem = grid.state.children[grid.state.children.length - 1] as DashboardGridItem;

      expect(newGridItem.state.height).toBe(1);
      expect(newGridItem.state.itemHeight).toBe(1);
    });

    it('Should duplicate a repeated panel', () => {
      const { manager, grid } = setup();
      const gItem = grid.state.children[0] as DashboardGridItem;
      gItem.setState({ variableName: 'server', repeatDirection: 'v', maxPerRow: 100 });
      const vizPanel = gItem.state.body;
      manager.duplicatePanel(vizPanel as VizPanel);

      const newGridItem = grid.state.children[grid.state.children.length - 1] as DashboardGridItem;

      expect(newGridItem.state.variableName).toBe('server');
      expect(newGridItem.state.repeatDirection).toBe('v');
      expect(newGridItem.state.maxPerRow).toBe(100);
    });

    it('Should duplicate a panel in a row', () => {
      const { manager } = setup();
      const vizPanel = findVizPanelByKey(manager, 'panel-within-row1')!;
      const gridRow = vizPanel.parent?.parent as SceneGridRow;

      expect(gridRow.state.children.length).toBe(2);

      manager.duplicatePanel(vizPanel);

      expect(gridRow.state.children.length).toBe(3);
    });
  });

  describe('duplicate', () => {
    it('returns a new DefaultGridLayoutManager instance', () => {
      const { manager } = setup();

      const duplicated = manager.duplicate() as DefaultGridLayoutManager;

      expect(duplicated).toBeInstanceOf(DefaultGridLayoutManager);
      expect(duplicated).not.toBe(manager);
      expect(duplicated.state.key).not.toBe(manager.state.key);
    });

    it('deep-clones all children', () => {
      const { manager, grid } = setup();
      const originalChildren = grid.state.children;

      const duplicated = manager.duplicate() as DefaultGridLayoutManager;
      const clonedChildren = duplicated.state.grid.state.children;

      expect(clonedChildren.length).toBe(originalChildren.length);

      expect(clonedChildren[0]).not.toBe(originalChildren[0]);
      expect((clonedChildren[0] as DashboardGridItem).state.body).not.toBe(
        (originalChildren[0] as DashboardGridItem).state.body
      );

      expect(clonedChildren[1]).not.toBe(originalChildren[1]);
      expect((clonedChildren[1] as DashboardGridItem).state.body).not.toBe(
        (originalChildren[1] as DashboardGridItem).state.body
      );

      expect(clonedChildren[2]).not.toBe(originalChildren[2]);
    });

    describe('when grid items contain panels', () => {
      it('assigns unique sequential panel keys, starting after the highest existing id', () => {
        const gridItems = [
          new DashboardGridItem({
            key: 'griditem-1',
            body: new VizPanel({
              key: 'panel-39',
              title: 'Panel A',
              pluginId: 'table',
            }),
          }),
          new DashboardGridItem({
            key: 'griditem-2',
            body: new VizPanel({
              key: 'panel-40',
              title: 'Panel B',
              pluginId: 'table',
            }),
          }),
        ];
        const { manager } = setup({ gridItems });

        const duplicated = manager.duplicate() as DefaultGridLayoutManager;

        const panelKeys = duplicated.getVizPanels().map((p) => p.state.key);
        expect(panelKeys).toEqual(['panel-41', 'panel-42']);
      });
    });
  });

  describe('createFromLayout', () => {
    it('preserves panel pluginId, title and options when creating from AutoGridLayoutManager', () => {
      const autoLayout = setupSceneWithAutoGrid();

      const defaultLayout = DefaultGridLayoutManager.createFromLayout(autoLayout);

      const children = defaultLayout.state.grid.state.children;
      expect(children).toHaveLength(1);
      const panel = (children[0] as DashboardGridItem).state.body;
      expect(panel.state.pluginId).toBe(panelPluginId);
      expect(panel.state.title).toBe(panelTitle);
      expect(panel.state.options).toEqual(panelOptions);
    });

    it('preserves panel queries when creating from AutoGridLayoutManager', () => {
      const autoLayout = setupSceneWithAutoGrid();

      const defaultLayout = DefaultGridLayoutManager.createFromLayout(autoLayout);

      const children = defaultLayout.state.grid.state.children;
      const panel = (children[0] as DashboardGridItem).state.body;
      const runner = getQueryRunnerFor(panel);
      expect(runner).toBeDefined();
      expect(runner?.state.queries).toEqual(queries);
    });

    it('preserves order of panels when creating DefaultGridLayoutManager from AutoGridLayoutManager', () => {
      const panelA = new VizPanel({ key: 'panel-a', title: 'Panel A', pluginId: 'table' });
      const panelB = new VizPanel({ key: 'panel-b', title: 'Panel B', pluginId: 'timeseries' });
      const panelC = new VizPanel({ key: 'panel-c', title: 'Panel C', pluginId: 'stat' });

      const autoLayout = setupSceneWithAutoGrid([panelA, panelB, panelC]);

      const defaultLayout = DefaultGridLayoutManager.createFromLayout(autoLayout);

      const children = defaultLayout.state.grid.state.children;
      expect(children).toHaveLength(3);
      expect(children[0]).toBeInstanceOf(DashboardGridItem);
      expect(children[1]).toBeInstanceOf(DashboardGridItem);
      expect(children[2]).toBeInstanceOf(DashboardGridItem);
      expect((children[0] as DashboardGridItem).state.body.state.title).toBe('Panel A');
      expect((children[1] as DashboardGridItem).state.body.state.title).toBe('Panel B');
      expect((children[2] as DashboardGridItem).state.body.state.title).toBe('Panel C');
    });

    it('preserves repeat variable (variableName) when creating DefaultGridLayoutManager from AutoGridLayoutManager', () => {
      const variableName = 'myRepeatVar';

      const dashboard = new DashboardScene({
        $variables: new SceneVariableSet({ variables: [] }),
        $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
        isEditing: true,
        body: new AutoGridLayoutManager({
          layout: new AutoGridLayout({
            children: [new AutoGridItem({ key: `auto-grid-item-1`, body: getDefaultVizPanel(), variableName })],
          }),
        }),
      });

      const defaultLayout = DefaultGridLayoutManager.createFromLayout(dashboard.state.body);

      const children = defaultLayout.state.grid.state.children;
      expect(children).toHaveLength(1);
      expect(children[0]).toBeInstanceOf(DashboardGridItem);
      expect((children[0] as DashboardGridItem).state.variableName).toBe(variableName);
    });

    it('does not set variableName when DashboardGridItem has no repeat variable', () => {
      const autoLayout = setupSceneWithAutoGrid();
      const defaultLayout = DefaultGridLayoutManager.createFromLayout(autoLayout);

      const children = defaultLayout.state.grid.state.children;
      expect(children).toHaveLength(1);
      expect((children[0] as DashboardGridItem).state.variableName).toBeUndefined();
    });
  });
});

interface TestOptions {
  gridItems: SceneGridItemLike[];
}

function setup(options?: TestOptions) {
  const gridItems = options?.gridItems ?? [
    new DashboardGridItem({
      key: 'griditem-1',
      x: 0,
      body: new VizPanel({
        title: 'Panel A',
        key: 'panel-1',
        pluginId: 'table',
        $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
      }),
    }),
    new DashboardGridItem({
      key: 'griditem-2',
      body: new VizPanel({
        title: 'Panel B',
        key: 'panel-2',
        pluginId: 'table',
      }),
    }),
    new SceneGridRow({
      key: 'panel-3',
      title: 'row',
      children: [
        new DashboardGridItem({
          body: new VizPanel({
            title: 'Panel C',
            key: 'panel-within-row1',
            pluginId: 'table',
          }),
        }),
        new DashboardGridItem({
          body: new VizPanel({
            title: 'Panel D',
            key: 'panel-within-row2',
            pluginId: 'table',
          }),
        }),
      ],
    }),
  ];

  const grid = new SceneGridLayout({ children: gridItems });
  const manager = new DefaultGridLayoutManager({ grid: grid });

  new DashboardScene({ body: manager });

  return { manager, grid };
}

const panelOptions = { legend: { displayMode: 'list', placement: 'bottom' } };
const panelTitle = 'Test panel';
const panelPluginId = 'timeseries';
const queries = [{ refId: 'A', datasource: { type: 'test', uid: 'ds1' } }];

const getDefaultVizPanel = () =>
  new VizPanel({
    key: 'panel-1',
    pluginId: panelPluginId,
    title: panelTitle,
    options: panelOptions,
    $data: new SceneQueryRunner({ key: 'test', queries }),
  });

function setupSceneWithAutoGrid(panels = [getDefaultVizPanel()], sceneOptions?: Partial<DashboardSceneState>) {
  const dashboard = new DashboardScene({
    $variables: new SceneVariableSet({ variables: [] }),
    $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
    ...sceneOptions,
    body: new AutoGridLayoutManager({
      layout: new AutoGridLayout({
        children: panels.map((panel, index) => new AutoGridItem({ key: `auto-grid-item-${index + 1}`, body: panel })),
      }),
    }),
  });
  return dashboard.state.body;
}
