/**
 * This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all
 * mocked (via vi.fn()) so you can track if they have been called, etc.
 *
 * Note that node() returns a HTML Element with tag 'svg'. It is an empty element (no innerHTML, no children, etc).
 * This potentially allows testing of mermaidAPI render().
 */
export class MockedD3 {
  public attribs = new Map<string, string>();
  public id: string | undefined = '';
  _children: MockedD3[] = [];

  _containingHTMLdoc = new Document();

  constructor(givenId = 'mock-id') {
    this.id = givenId;
  }

  /** Helpful utility during development/debugging. This is not a real d3 function */
  public listChildren(): string {
    return this._children
      .map((child) => {
        return child.id;
      })
      .join(', ');
  }

  select = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
    // Get the id from an argument string. if it is of the form [id='some-id'], strip off the
    // surrounding id[..]
    const stripSurroundRegexp = /\[id='(.*)']/;
    const matchedSurrounds = select_str.match(stripSurroundRegexp);
    const cleanId = matchedSurrounds ? matchedSurrounds[1] : select_str;
    return new MockedD3(cleanId);
  });

  // This has the same implementation as select(). (It calls it.)
  selectAll = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
    return this.select(select_str);
  });

  append = vi.fn().mockImplementation(function (
    this: MockedD3,
    type: string,
    id = '' + '-appended'
  ): MockedD3 {
    const newMock = new MockedD3(id);
    newMock.attribs.set('type', type);
    this._children.push(newMock);
    return newMock;
  });

  // NOTE: The d3 implementation allows for a selector ('beforeSelector' arg below).
  //   With this mocked implementation, we assume it will always refer to an node id
  //   and will always be of the form "#[id of the node to insert before]".
  //   To keep this simple, any leading '#' is removed and the resulting string is the node id searched.
  insert = (type: string, beforeSelector?: string, id = this.id + '-inserted'): MockedD3 => {
    const newMock = new MockedD3(id);
    newMock.attribs.set('type', type);
    if (beforeSelector === undefined) {
      this._children.push(newMock);
    } else {
      const idOnly = beforeSelector.startsWith('#') ? beforeSelector.substring(1) : beforeSelector;
      const foundIndex = this._children.findIndex((child) => child.id === idOnly);
      if (foundIndex < 0) {
        this._children.push(newMock);
      } else {
        this._children.splice(foundIndex, 0, newMock);
      }
    }
    return newMock;
  };

  attr(attrName: string): undefined | string;
  attr(attrName: string, attrValue: string): MockedD3;
  attr(attrName: string, attrValue?: string): undefined | string | MockedD3 {
    if (arguments.length === 1) {
      return this.attribs.get(attrName);
    } else {
      if (attrName === 'id') {
        this.id = attrValue; // also set the id explicitly
      }
      if (attrValue !== undefined) {
        this.attribs.set(attrName, attrValue);
      }
      return this;
    }
  }

  public lower(attrValue = '') {
    this.attribs.set('lower', attrValue);
    return this;
  }
  public style(attrValue = '') {
    this.attribs.set('style', attrValue);
    return this;
  }
  public text(attrValue = '') {
    this.attribs.set('text', attrValue);
    return this;
  }

  // NOTE: Returns a HTML Element with tag 'svg' that has _another_ 'svg' element child.
  // This allows different tests to succeed -- some need a top level 'svg' and some need a 'svg' element to be the firstChild
  // Real implementation returns an HTML Element
  public node = vi.fn().mockImplementation(() => {
    //create a top level svg element
    const topElem = this._containingHTMLdoc.createElement('svg');
    //@ts-ignore - this is a mock SVG element
    topElem.getBBox = this.getBBox;
    const elem_svgChild = this._containingHTMLdoc.createElement('svg'); // another svg element
    topElem.appendChild(elem_svgChild);
    return topElem;
  });

  // TODO Is this correct? shouldn't it return a list of HTML Elements?
  nodes = vi.fn().mockImplementation(function (this: MockedD3): MockedD3[] {
    return this._children;
  });

  // This will try to use attrs that have been set.
  getBBox = () => {
    const x = this.attribs.has('x') ? this.attribs.get('x') : 20;
    const y = this.attribs.has('y') ? this.attribs.get('y') : 30;
    const width = this.attribs.has('width') ? this.attribs.get('width') : 140;
    const height = this.attribs.has('height') ? this.attribs.get('height') : 250;
    return {
      x: x,
      y: y,
      width: width,
      height: height,
    };
  };

  // --------------------------------------------------------------------------------
  // The following functions are here for completeness.  They simply return a vi.fn()

  insertBefore = vi.fn();
  curveBasis = vi.fn();
  curveBasisClosed = vi.fn();
  curveBasisOpen = vi.fn();
  curveLinear = vi.fn();
  curveLinearClosed = vi.fn();
  curveMonotoneX = vi.fn();
  curveMonotoneY = vi.fn();
  curveNatural = vi.fn();
  curveStep = vi.fn();
  curveStepAfter = vi.fn();
  curveStepBefore = vi.fn();
}
