import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing';
import { clickOnElement } from '../../internal/test.js';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type SlTree from './tree.component.js';
import type SlTreeItem from '../tree-item/tree-item.js';

describe('<sl-tree>', () => {
  let el: SlTree;

  beforeEach(async () => {
    el = await fixture(html`
      <sl-tree>
        <sl-tree-item>Node 1</sl-tree-item>
        <sl-tree-item>Node 2</sl-tree-item>
        <sl-tree-item id="expandable">
          Parent Node
          <sl-tree-item>Child Node 1</sl-tree-item>
          <sl-tree-item>
            Child Node 2
            <sl-tree-item>Child Node 2 - 1</sl-tree-item>
            <sl-tree-item>Child Node 2 - 2</sl-tree-item>
          </sl-tree-item>
        </sl-tree-item>
        <sl-tree-item>Node 3</sl-tree-item>
      </sl-tree>
    `);
  });

  it('should render a component', () => {
    expect(el).to.exist;
    expect(el).to.have.attribute('role', 'tree');
    expect(el).to.have.attribute('tabindex', '0');
  });

  it('should pass accessibility tests', async () => {
    await expect(el).to.be.accessible();
  });

  it('should not focus collapsed nodes', async () => {
    // Arrange
    const parentNode = el.children[2] as SlTreeItem;
    const childNode = parentNode.children[1] as SlTreeItem;
    childNode.expanded = true;
    parentNode.expanded = false;

    await el.updateComplete;

    // Act
    const focusableItems = el.getFocusableItems();

    // Assert
    expect(focusableItems).to.have.lengthOf(4);
    expect(focusableItems).not.to.include.all.members([childNode, ...childNode.children]);
    expect(focusableItems).not.to.include.all.members([...parentNode.children]);
  });

  describe('when a custom expanded/collapsed icon is provided', () => {
    beforeEach(async () => {
      el = await fixture(html`
        <sl-tree>
          <div slot="expand-icon"></div>
          <div slot="collapse-icon"></div>

          <sl-tree-item>Node 1</sl-tree-item>
          <sl-tree-item>Node 2</sl-tree-item>
        </sl-tree>
      `);
    });

    it('should append a clone of the icon in the proper slot of the tree item', async () => {
      // Arrange
      await el.updateComplete;

      // Act
      const treeItems = [...el.querySelectorAll('sl-tree-item')];

      // Assert
      treeItems.forEach(treeItem => {
        expect(treeItem.querySelector('div[slot="expand-icon"]')).to.be.ok;
        expect(treeItem.querySelector('div[slot="collapse-icon"]')).to.be.ok;
      });
    });
  });

  describe('Keyboard navigation', () => {
    describe('when ArrowDown is pressed', () => {
      it('should move the focus to the next tree item', async () => {
        // Arrange
        el.focus();
        await el.updateComplete;

        // Act
        await sendKeys({ press: 'ArrowDown' });

        // Assert
        expect(el).to.have.attribute('tabindex', '-1');
        expect(el.children[0]).to.have.attribute('tabindex', '-1');
        expect(el.children[1]).to.have.attribute('tabindex', '0');
      });
    });

    describe('when ArrowUp is pressed', () => {
      it('should move the focus to the prev tree item', async () => {
        // Arrange
        (el.children[1] as HTMLElement).focus();
        await el.updateComplete;

        // Act
        await sendKeys({ press: 'ArrowUp' });

        // Assert
        expect(el).to.have.attribute('tabindex', '-1');
        expect(el.children[0]).to.have.attribute('tabindex', '0');
        expect(el.children[1]).to.have.attribute('tabindex', '-1');
      });
    });

    describe('when ArrowRight is pressed', () => {
      describe('and node is a leaf', () => {
        it('should move the focus to the next tree item', async () => {
          // Arrange
          (el.children[0] as HTMLElement).focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'ArrowRight' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(el.children[0]).to.have.attribute('tabindex', '-1');
          expect(el.children[1]).to.have.attribute('tabindex', '0');
        });
      });

      describe('and node is collapsed', () => {
        it('should expand the tree item', async () => {
          // Arrange
          const parentNode = el.children[2] as SlTreeItem;
          parentNode.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'ArrowRight' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(parentNode).to.have.attribute('tabindex', '0');
          expect(parentNode).to.have.attribute('expanded');
        });
      });

      describe('and node is expanded', () => {
        it('should move the focus to the next tree item', async () => {
          // Arrange
          const parentNode = el.children[2] as SlTreeItem;
          parentNode.expanded = true;
          parentNode.focus();

          await el.updateComplete;

          // Act
          await sendKeys({ press: 'ArrowRight' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(parentNode).to.have.attribute('tabindex', '-1');
          expect(parentNode.children[0]).to.have.attribute('tabindex', '0');
        });
      });
    });

    describe('when ArrowLeft is pressed', () => {
      describe('and node is a leaf', () => {
        it('should move the focus to the prev tree item', async () => {
          // Arrange
          (el.children[1] as HTMLElement).focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'ArrowLeft' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(el.children[0]).to.have.attribute('tabindex', '0');
          expect(el.children[1]).to.have.attribute('tabindex', '-1');
        });
      });

      describe('and node is collapsed', () => {
        it('should move the focus to the prev tree item', async () => {
          // Arrange
          (el.children[2] as HTMLElement).focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'ArrowLeft' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(el.children[1]).to.have.attribute('tabindex', '0');
          expect(el.children[2]).to.have.attribute('tabindex', '-1');
        });
      });

      describe('and node is expanded', () => {
        it('should collapse the tree item', async () => {
          // Arrange
          const parentNode = el.children[2] as SlTreeItem;
          parentNode.expanded = true;
          parentNode.focus();

          await el.updateComplete;

          // Act
          await sendKeys({ press: 'ArrowLeft' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(parentNode).to.have.attribute('tabindex', '0');
          expect(parentNode).not.to.have.attribute('expanded');
        });
      });
    });

    describe('when Home is pressed', () => {
      it('should move the focus to the first tree item in the tree', async () => {
        // Arrange
        const parentNode = el.children[3] as SlTreeItem;
        parentNode.focus();
        await el.updateComplete;

        // Act
        await sendKeys({ press: 'Home' });

        // Assert
        expect(el).to.have.attribute('tabindex', '-1');
        expect(el.children[0]).to.have.attribute('tabindex', '0');
        expect(el.children[3]).to.have.attribute('tabindex', '-1');
      });
    });

    describe('when End is pressed', () => {
      it('should move the focus to the last tree item in the tree', async () => {
        // Arrange
        const parentNode = el.children[0] as SlTreeItem;
        parentNode.focus();
        await el.updateComplete;

        // Act
        await sendKeys({ press: 'End' });

        // Assert
        expect(el).to.have.attribute('tabindex', '-1');
        expect(el.children[0]).to.have.attribute('tabindex', '-1');
        expect(el.children[3]).to.have.attribute('tabindex', '0');
      });
    });

    describe('when Enter is pressed', () => {
      describe('and selection is "single"', () => {
        it('should select only one tree item', async () => {
          // Arrange
          el.selection = 'single';
          const node = el.children[1] as SlTreeItem;
          node.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'Enter' });
          await sendKeys({ press: 'ArrowRight' });
          await sendKeys({ press: 'Enter' });

          // Assert
          expect(el.selectedItems.length).to.eq(1);
          expect(el.children[2]).to.have.attribute('selected');
        });
      });

      describe('and selection is "leaf"', () => {
        it('should select only one tree item', async () => {
          // Arrange
          el.selection = 'leaf';
          const node = el.children[0] as SlTreeItem;
          node.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'Enter' });
          await sendKeys({ press: 'ArrowRight' });
          await sendKeys({ press: 'Enter' });

          // Assert
          expect(el.selectedItems.length).to.eq(1);
        });

        it('should expand/collapse a parent node', async () => {
          // Arrange
          el.selection = 'leaf';
          const parentNode = el.children[2] as SlTreeItem;
          parentNode.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'Enter' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(el.selectedItems.length).to.eq(0);
          expect(parentNode).to.have.attribute('expanded');
        });
      });

      describe('and selection is "multiple"', () => {
        it('should toggle the selection on the tree item', async () => {
          // Arrange
          el.selection = 'multiple';
          const node = el.children[1] as SlTreeItem;
          node.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: 'Enter' });
          await sendKeys({ press: 'ArrowRight' });
          await sendKeys({ press: 'Enter' });

          // Assert
          expect(el.selectedItems.length).to.eq(6);
        });
      });
    });

    describe('when Space is pressed', () => {
      describe('and selection is "single"', () => {
        it('should select only one tree item', async () => {
          // Arrange
          el.selection = 'single';
          const node = el.children[1] as SlTreeItem;
          node.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: ' ' });
          await sendKeys({ press: 'ArrowRight' });
          await sendKeys({ press: ' ' });

          // Assert
          expect(el.selectedItems.length).to.eq(1);
        });
      });

      describe('and selection is "leaf"', () => {
        it('should select only one tree item', async () => {
          // Arrange
          el.selection = 'leaf';
          const node = el.children[0] as SlTreeItem;
          node.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: ' ' });
          await sendKeys({ press: 'ArrowRight' });
          await sendKeys({ press: ' ' });

          // Assert
          expect(el.selectedItems.length).to.eq(1);
        });

        it('should expand/collapse a parent node', async () => {
          // Arrange
          el.selection = 'leaf';
          const parentNode = el.children[2] as SlTreeItem;
          parentNode.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: ' ' });

          // Assert
          expect(el).to.have.attribute('tabindex', '-1');
          expect(el.selectedItems.length).to.eq(0);
          expect(parentNode).to.have.attribute('expanded');
        });
      });

      describe('and selection is "multiple"', () => {
        it('should toggle the selection on the tree item', async () => {
          // Arrange
          el.selection = 'multiple';
          const node = el.children[0] as SlTreeItem;
          node.focus();
          await el.updateComplete;

          // Act
          await sendKeys({ press: ' ' });
          await sendKeys({ press: 'ArrowRight' });
          await sendKeys({ press: ' ' });

          // Assert
          expect(el.selectedItems.length).to.eq(2);
        });
      });
    });
  });

  describe('Interactions', () => {
    describe('when the tree is about to receive the focus', () => {
      it('should set the focus to the last focused item', async () => {
        // Arrange
        const node = el.children[1] as SlTreeItem;
        node.focus();
        await el.updateComplete;

        // Act
        triggerBlurFor(node);
        triggerFocusFor(el);

        // Assert
        expect(el).to.have.attribute('tabindex', '-1');
        expect(node).to.have.attribute('tabindex', '0');
      });
    });

    describe('when the user clicks the expand button', () => {
      it('should expand the tree item', async () => {
        // Arrange
        el.selection = 'single';
        await el.updateComplete;

        const node = el.children[2] as SlTreeItem;
        await node.updateComplete;

        const expandButton: HTMLElement = node.shadowRoot!.querySelector('.tree-item__expand-button')!;

        // Act
        await clickOnElement(expandButton);
        await el.updateComplete;

        // Assert
        expect(node).to.have.attribute('expanded');
      });
    });

    describe('when the user clicks on a tree item', () => {
      describe('and selection is "single"', () => {
        it('should select only one tree item', async () => {
          // Arrange
          el.selection = 'single';
          const node0 = el.children[0] as SlTreeItem;
          const node1 = el.children[1] as SlTreeItem;

          await el.updateComplete;

          // Act
          await clickOnElement(node0);
          await el.updateComplete;

          await clickOnElement(node1);
          await el.updateComplete;

          // Assert
          expect(el.selectedItems.length).to.eq(1);
        });
      });

      describe('and selection is "leaf"', () => {
        it('should select only one tree item', async () => {
          // Arrange
          el.selection = 'leaf';
          const node0 = el.children[0] as SlTreeItem;
          const node1 = el.children[1] as SlTreeItem;

          await el.updateComplete;

          // Act
          await clickOnElement(node0);
          await el.updateComplete;

          await clickOnElement(node1);
          await el.updateComplete;

          // Assert
          expect(el.selectedItems.length).to.eq(1);
        });

        it('should expand/collapse a parent node', async () => {
          // Arrange
          el.selection = 'leaf';
          const parentNode = el.children[2] as SlTreeItem;

          await el.updateComplete;

          // Act
          await clickOnElement(parentNode);
          await parentNode.updateComplete;

          // Assert
          expect(el.selectedItems.length).to.eq(0);
          expect(parentNode).to.have.attribute('expanded');
        });
      });

      describe('and selection is "multiple"', () => {
        it('should toggle the selection on the tree item', async () => {
          // Arrange
          el.selection = 'multiple';
          const node0 = el.children[0] as SlTreeItem;
          const node1 = el.children[1] as SlTreeItem;

          await el.updateComplete;

          // Act
          await clickOnElement(node0);
          await el.updateComplete;

          await clickOnElement(node1);
          await el.updateComplete;

          // Assert
          expect(el.selectedItems.length).to.eq(2);
        });

        it('should select all the child tree items', async () => {
          // Arrange
          el.selection = 'multiple';
          await el.updateComplete;

          const parentNode = el.children[2] as SlTreeItem;

          // Act
          await clickOnElement(parentNode);
          await el.updateComplete;

          // Assert
          expect(parentNode).to.have.attribute('selected');
          expect(parentNode.indeterminate).to.be.false;
          parentNode.getChildrenItems().forEach(child => {
            expect(child).to.have.attribute('selected');
          });
        });

        it('should set the indeterminate state to tree items if a child is selected', async () => {
          // Arrange
          el.selection = 'multiple';
          await el.updateComplete;

          const parentNode = el.children[2] as SlTreeItem;
          const childNode = parentNode.children[0] as SlTreeItem;

          // Act
          parentNode.expanded = true;
          await parentNode.updateComplete;
          await aTimeout(300);
          await clickOnElement(childNode);
          await el.updateComplete;

          // Assert
          expect(parentNode).not.to.have.attribute('selected');
          expect(parentNode.indeterminate).to.be.true;
        });
      });
    });

    describe('when selection is "single"', () => {
      describe('and user clicks on same item twice', () => {
        it('should emit `sl-selection-change` event once', async () => {
          // Arrange
          el.selection = 'single';
          await el.updateComplete;

          const selectedChangeSpy = sinon.spy();
          el.addEventListener('sl-selection-change', selectedChangeSpy);

          const node = el.children[0] as SlTreeItem;

          // Act
          await clickOnElement(node);
          await el.updateComplete;
          await clickOnElement(node);
          await Promise.all([node.updateComplete, el.updateComplete]);

          // Assert
          expect(selectedChangeSpy).to.have.been.calledOnce;
          expect(selectedChangeSpy.args[0][0]).to.deep.include({ detail: { selection: [node] } });
        });
      });
    });
  });

  describe('when selection is "leaf"', () => {
    describe('and user clicks on same leaf item twice', () => {
      it('should emit `sl-selection-change` event once', async () => {
        // Arrange
        el.selection = 'leaf';
        await el.updateComplete;

        const selectedChangeSpy = sinon.spy();
        el.addEventListener('sl-selection-change', selectedChangeSpy);

        const node = el.children[0] as SlTreeItem;

        // Act
        await clickOnElement(node);
        await el.updateComplete;
        await clickOnElement(node);
        await Promise.all([node.updateComplete, el.updateComplete]);

        // Assert
        expect(selectedChangeSpy).to.have.been.calledOnce;
        expect(selectedChangeSpy.args[0][0]).to.deep.include({ detail: { selection: [node] } });
      });
    });

    describe('and user clicks on expandable item', () => {
      it('should not emit `sl-selection-change` event', async () => {
        // Arrange
        el.selection = 'leaf';
        await el.updateComplete;

        const selectedChangeSpy = sinon.spy();
        el.addEventListener('sl-selection-change', selectedChangeSpy);

        const node = el.querySelector<SlTreeItem>('#expandable')!;

        // Act
        await clickOnElement(node);
        await Promise.all([node.updateComplete, el.updateComplete]);

        // Assert
        expect(selectedChangeSpy).to.not.have.been.called;
      });
    });
  });

  describe('when selection is "multiple"', () => {
    describe('and user clicks on same item twice', () => {
      it('should emit `sl-selection-change` event twice', async () => {
        // Arrange
        el.selection = 'multiple';
        await el.updateComplete;

        const selectedChangeSpy = sinon.spy();
        el.addEventListener('sl-selection-change', selectedChangeSpy);

        const node = el.children[0] as SlTreeItem;

        // Act
        await clickOnElement(node);
        await Promise.all([node.updateComplete, el.updateComplete]);
        await clickOnElement(node);
        await Promise.all([node.updateComplete, el.updateComplete]);

        // Assert
        expect(selectedChangeSpy).to.have.been.calledTwice;
        expect(selectedChangeSpy.args[0][0]).to.deep.include({ detail: { selection: [node] } });
        expect(selectedChangeSpy.args[1][0]).to.deep.include({ detail: { selection: [] } });
      });
    });
  });

  describe('Checkboxes synchronization', () => {
    describe('when the tree gets initialized', () => {
      describe('and a parent node is selected', () => {
        it('should select all the nested children', async () => {
          // Arrange
          const tree = await fixture<SlTree>(html`
            <sl-tree selection="multiple">
              <sl-tree-item selected>
                Parent Node
                <sl-tree-item selected>Child Node 1</sl-tree-item>
                <sl-tree-item>
                  Child Node 2
                  <sl-tree-item>Child Node 2 - 1</sl-tree-item>
                  <sl-tree-item>Child Node 2 - 2</sl-tree-item>
                </sl-tree-item>
              </sl-tree-item>
            </sl-tree>
          `);
          const treeItems = Array.from<SlTreeItem>(tree.querySelectorAll('sl-tree-item'));

          // Act
          await tree.updateComplete;

          // Assert
          treeItems.forEach(treeItem => {
            expect(treeItem).to.have.attribute('selected');
          });
        });
      });

      describe('and a parent node is not selected', () => {
        describe('and all the children are selected', () => {
          it('should select the parent node', async () => {
            // Arrange
            const tree = await fixture<SlTree>(html`
              <sl-tree selection="multiple">
                <sl-tree-item>
                  Parent Node
                  <sl-tree-item selected>Child Node 1</sl-tree-item>
                  <sl-tree-item selected>
                    Child Node 2
                    <sl-tree-item>Child Node 2 - 1</sl-tree-item>
                    <sl-tree-item>Child Node 2 - 2</sl-tree-item>
                  </sl-tree-item>
                </sl-tree-item>
              </sl-tree>
            `);
            const treeItems = Array.from<SlTreeItem>(tree.querySelectorAll('sl-tree-item'));

            // Act
            await tree.updateComplete;

            // Assert
            treeItems.forEach(treeItem => {
              expect(treeItem).to.have.attribute('selected');
            });
            expect(treeItems[0].indeterminate).to.be.false;
          });
        });

        describe('and some of the children are selected', () => {
          it('should set the parent node to indeterminate state', async () => {
            // Arrange
            const tree = await fixture<SlTree>(html`
              <sl-tree selection="multiple">
                <sl-tree-item>
                  Parent Node
                  <sl-tree-item selected>Child Node 1</sl-tree-item>
                  <sl-tree-item>
                    Child Node 2
                    <sl-tree-item>Child Node 2 - 1</sl-tree-item>
                    <sl-tree-item>Child Node 2 - 2</sl-tree-item>
                  </sl-tree-item>
                </sl-tree-item>
              </sl-tree>
            `);
            const treeItems = Array.from<SlTreeItem>(tree.querySelectorAll('sl-tree-item'));

            // Act
            await tree.updateComplete;

            // Assert
            expect(treeItems[0]).not.to.have.attribute('selected');
            expect(treeItems[0].indeterminate).to.be.true;
            expect(treeItems[1]).to.have.attribute('selected');
            expect(treeItems[2]).not.to.have.attribute('selected');
            expect(treeItems[3]).not.to.have.attribute('selected');
            expect(treeItems[4]).not.to.have.attribute('selected');
          });
        });
      });
    });
  });
});
