import { cloneDeep } from 'lodash-es'

const username = Cypress.env('username')
const password = Cypress.env('password')
const hostUrl = Cypress.env('hosturl')
const appLocation = Cypress.env('appLocation')
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
const serverType = Cypress.env('serverType')
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)

context('editor tests: ', function () {
  this.beforeAll(() => {
    cy.visit(`${hostUrl}/SASLogon/logout`)
    cy.loginAndUpdateValidKey(true)
  })

  this.beforeEach(() => {
    cy.visit(hostUrl + appLocation)
    cy.wait(2000)

    cy.get('body').then(($body) => {
      const usernameInput = $body.find('input.username')[0]

      if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
        cy.get('input.username').type(username)
        cy.get('input.password').type(password)

        cy.get('.login-group button').click()
      }
    })

    visitPage('home')
  })

  it('1 | Add one viewbox', (done) => {
    const viewbox_table = 'mpe_audit'
    const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']

    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    cy.get('.viewbox-open').click()
    openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])

    cy.contains('.open-viewbox', viewbox_table, {
      matchCase: false,
      timeout: longerCommandTimeout
    })

    checkColumns(columns, () => {
      done()
    })
  })

  it('2 | Add two viewboxes', (done) => {
    const viewboxes = [
      {
        viewbox_table: 'mpe_audit',
        columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
      },
      {
        viewbox_table: 'mpe_alerts',
        columns: [
          'TX_FROM',
          'ALERT_EVENT',
          'ALERT_LIB',
          'ALERT_DS',
          'ALERT_USER'
        ]
      }
    ]

    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
    cy.get('.viewbox-open').click()
    openTableFromViewboxTree(
      libraryToOpenIncludes,
      viewboxes.map((viewbox) => viewbox.viewbox_table)
    )
    viewboxes.forEach((viewbox) => {
      cy.contains('.open-viewbox', viewbox.viewbox_table, {
        matchCase: false,
        timeout: longerCommandTimeout
      })
    })

    let result = 0

    viewboxes.forEach((viewbox) => {
      checkColumns(viewbox.columns, () => {
        result++

        if (result === viewboxes.length) done()
      })
    })
  })

  it('3 | Add viewbox, add columns', (done) => {
    const viewbox_table = 'mpe_audit'
    const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
    const additionalColumns = ['IS_PK']

    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    cy.get('.viewbox-open').click()
    openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])

    cy.contains('.open-viewbox', viewbox_table, {
      matchCase: false,
      timeout: longerCommandTimeout
    })

    openViewboxConfig(viewbox_table)

    removeAllColumns()

    addColumns(additionalColumns)

    checkColumns([...columns, ...additionalColumns], () => {
      done()
    })
  })

  it('4 | Add viewbox, add columns and reorder', (done) => {
    const viewbox_table = 'mpe_audit'
    const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
    const additionalColumns = ['IS_PK', 'MOVE_TYPE']

    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    cy.get('.viewbox-open').click()
    openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])

    cy.contains('.open-viewbox', viewbox_table, {
      matchCase: false,
      timeout: longerCommandTimeout
    })

    openViewboxConfig(viewbox_table)

    removeAllColumns()

    addColumns(additionalColumns, () => {
      cy.wait(1000)
      //reorder
      cy.get('.col-box.column-MOVE_TYPE')
        .realMouseDown({ button: 'left', position: 'center' })
        .realMouseMove(0, 10, { position: 'center' })
      cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
      cy.get('.col-box.column-IS_PK')
        .realMouseMove(0, 0, { position: 'center' })
        .realMouseUp()
      //reorder end

      cy.wait(500)

      checkColumns([...columns, ...additionalColumns.reverse()], () => {
        done()
      })
    })
  })

  it('5 | Add viewbox, add columns, reorder, remove column, add again', (done) => {
    const viewbox_table = 'mpe_audit'
    const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
    const additionalColumns = ['IS_PK', 'MOVE_TYPE']

    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    cy.get('.viewbox-open').click()
    openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])

    cy.contains('.open-viewbox', viewbox_table, {
      matchCase: false,
      timeout: longerCommandTimeout
    })

    openViewboxConfig(viewbox_table)

    removeAllColumns()

    addColumns(additionalColumns, () => {
      cy.wait(1000)
      //reorder
      cy.get('.col-box.column-MOVE_TYPE')
        .realMouseDown({ button: 'left', position: 'center' })
        .realMouseMove(0, 10, { position: 'center' })
      cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
      cy.get('.col-box.column-IS_PK')
        .realMouseMove(0, 0, { position: 'center' })
        .realMouseUp()
      //reorder end

      cy.wait(500)

      checkColumns([...columns, ...additionalColumns.reverse()], () => {
        const colToRemove = 'MOVE_TYPE'

        removeColumn(colToRemove)
        checkColumns(
          [
            ...columns,
            ...additionalColumns.filter((col) => col !== colToRemove)
          ],
          () => {
            addColumns([colToRemove], () => {
              checkColumns([...columns, ...additionalColumns.reverse()], () => {
                done()
              })
            })
          }
        )
      })
    })
  })

  it('6 | Add viewboxes, reload and check url restored configuration', (done) => {
    const viewboxes = [
      {
        viewbox_table: 'mpe_audit',
        columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM'],
        additionalColumns: ['IS_PK', 'MOVE_TYPE']
      },
      {
        viewbox_table: 'mpe_alerts',
        columns: [
          'TX_FROM',
          'ALERT_EVENT',
          'ALERT_LIB',
          'ALERT_DS',
          'ALERT_USER'
        ],
        additionalColumns: ['TX_TO']
      }
    ]

    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    cy.get('.viewbox-open').click()
    openTableFromViewboxTree(libraryToOpenIncludes, [
      viewboxes[0].viewbox_table,
      viewboxes[1].viewbox_table
    ])

    openViewboxConfig(viewboxes[0].viewbox_table)

    cy.wait(500)

    removeAllColumns()

    addColumns(viewboxes[0].additionalColumns, () => {
      cy.wait(1000)

      if (viewboxes[0].viewbox_table === 'mpe_audit') {
        cy.get('.col-box.column-MOVE_TYPE')
          .realMouseDown({ button: 'left', position: 'center' })
          .realMouseMove(0, 10, { position: 'center' })
        cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
        cy.get('.col-box.column-IS_PK')
          .realMouseMove(0, 0, { position: 'center' })
          .realMouseUp()
      }

      cy.wait(1000)

      openViewboxConfig(viewboxes[1].viewbox_table)

      addColumns(viewboxes[1].additionalColumns, () => {
        cy.wait(1000).reload()

        let result = 0

        checkColumns(
          [
            ...viewboxes[0].columns,
            ...cloneDeep(viewboxes[0].additionalColumns.reverse())
          ],
          () => {
            result++

            if (result === 2) done()
          }
        )
        checkColumns(
          [...viewboxes[1].columns, ...viewboxes[1].additionalColumns],
          () => {
            result++

            if (result === 2) done()
          }
        )
      })
    })
  })

  // The base VIEW table and a viewbox on a different table must never
  // share a filter dialog's cached state - each keeps its own entry in
  // the globals filter cache (getFilterObjPath), keyed by viewboxId where
  // one is present. Opened from the VIEW page specifically
  // (rootParam === 'view') - the one branch where this previously broke,
  // since a 'view'-page QueryComponent was always treated as the base
  // table regardless of viewboxId.
  it('7 | Base table filter and viewbox filter stay isolated from each other', () => {
    const viewbox_table = 'mpe_audit'

    visitPage('view/data')
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    // Filter the base table.
    openViewFilter()
    setFilterWithValue('SOME_CHAR', 'this is dummy data', 'value', () => {
      cy.get('app-query', { withinSubject: null }).should('not.exist')

      // Open a viewbox on a *different* table and check its filter dialog
      // does not show the base table's filter.
      openViewViewboxes()
      openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
      closeViewboxModal()

      // cy.contains retries until the viewbox's title actually shows the
      // expected table name (it can render before its data/title loads),
      // then .parents() walks back up to its container - avoids a
      // one-shot .then() racing the title's own async update, which is
      // what made this flaky in the first place.
      cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, {
        matchCase: false,
        timeout: longerCommandTimeout
      })
        .parents('.viewbox')
        .first()
        .within(() => {
          cy.get('clr-icon[shape="filter"]').click()

          cy.get('.filter-modal code.language-sql', {
            withinSubject: null
          }).should(($code) => {
            expect($code.text()).to.not.contain('SOME_CHAR')
          })

          // Reverse direction: filter the viewbox, then confirm
          // re-opening the base table's own filter dialog still shows
          // the base table's filter, not the viewbox's.
          setFilterWithValue('LIBREF', 'sasdemo', 'value', () => {
            cy.get('app-query', { withinSubject: null }).should('not.exist')

            openViewFilter()

            cy.get('.filter-modal code.language-sql', {
              withinSubject: null
            }).should(($code) => {
              expect($code.text()).to.contain('SOME_CHAR')
              expect($code.text()).to.not.contain('LIBREF')
            })
          })
        })
    })
  })
})

const removeAllColumns = () => {
  cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(
    (removeNodes) => {
      for (let removeNode of removeNodes) {
        removeNode.click()
      }
    }
  )
}

// Asserts that at least one open viewbox currently shows exactly this
// column list, in order. Uses a single .should() callback (synchronous
// jQuery reads only, no nested cy.get()/.then()) so Cypress retries the
// whole check against a fresh DOM snapshot until it passes or times out -
// unlike .then(), which only ever sees one snapshot and, combined with an
// early return on a mismatch, would silently never invoke the caller's
// callback while Handsontable/the viewbox's data are still loading or
// re-rendering asynchronously.
const checkColumns = (columns: string[], callback: () => void) => {
  cy.get('.viewboxes-container .viewbox', { withinSubject: null }).should(
    ($viewboxNodes) => {
      const matches = $viewboxNodes.toArray().some((viewboxNode) => {
        // rowHeaders: true (viewboxes.component.ts) adds a leading, unlabeled
        // corner <th> before the actual data columns - skip it, or every
        // comparison is off by one against the row-header cell instead of
        // the first real column.
        const headerCells = Cypress.$(viewboxNode)
          .find('.ht_master.handsontable thead tr th')
          .toArray()
          .slice(1)

        return columns.every((col, i) =>
          (headerCells[i]?.innerHTML || '')
            .toLowerCase()
            .includes(col.toLowerCase())
        )
      })

      expect(matches, `a viewbox showing columns: ${columns.join(', ')}`).to.be
        .true
    }
  )

  cy.then(() => callback())
}

const closeViewboxModal = () => {
  cy.get('app-viewboxes .close', { withinSubject: null }).click()
}

const removeColumn = (column: string) => {
  cy.get(`.col-box.column-${column} clr-icon`, { withinSubject: null }).click()
}

const addColumns = (columns: string[], callback?: () => void) => {
  for (const column of columns) {
    cy.get('.cols-search input', { withinSubject: null }).type(column)
    cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
      .first()
      .trigger('keydown', { key: 'ArrowDown' })
    cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
      .first()
      .trigger('keydown', { key: 'Enter' })

    // Wait for the actual column box to render before moving on to the
    // next column (or firing the callback) - selecting it doesn't add it
    // to the DOM synchronously, and cy.get()'s own retry covers that gap
    // instead of relying on a fixed cy.wait().
    cy.get(`.col-box.column-${column}`, { timeout: longerCommandTimeout })
  }

  if (callback) cy.then(() => callback())
}

// The VIEW page's "options" dropdown (viewer.component.html) - distinct
// from the editor page's dedicated .viewbox-open button used by the
// tests above, since test 7 runs on the VIEW page specifically.
const openViewOptionsDropdown = () => {
  cy.get('.filterSide', { withinSubject: null }).click()
}

const openViewFilter = () => {
  openViewOptionsDropdown()
  cy.get('[clrDropdownItem]', { withinSubject: null })
    .contains('Filter')
    .click()
}

const openViewViewboxes = () => {
  openViewOptionsDropdown()
  cy.get('[clrDropdownItem]', { withinSubject: null })
    .contains('Viewboxes')
    .click()
}

// cy.contains retries until a matching node appears/updates - the open
// viewbox's own list entry can render before its table-name text has
// actually loaded, so a one-shot .then() lookup here would race it.
const openViewboxConfig = (viewbox_tablename: string) => {
  cy.contains('.open-viewbox', viewbox_tablename, {
    matchCase: false,
    timeout: longerCommandTimeout
  }).click()
}

const openTableFromTree = (libNameIncludes: string, tablename: string) => {
  cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')

  // Small settle wait: right after a fresh visit (an extra navigation on
  // top of beforeEach's own, e.g. test 7's visitPage('view/data')), the
  // tree component can still be re-rendering, causing a node to be found
  // then swapped out mid-click - same fix already proven in
  // viewer-labels.cy.ts.
  cy.wait(300)

  const libraryNodeSelector = '.nav-tree clr-tree > clr-tree-node'
  const libraryMatcher = new RegExp(libNameIncludes, 'i')

  cy.contains(libraryNodeSelector, libraryMatcher, {
    timeout: longerCommandTimeout
  })
    .find('.clr-tree-node-content-container p')
    .click()

  // Re-query the library node fresh rather than reusing the reference
  // from before the click above - expanding a library can replace its
  // whole DOM subtree once its child tables load (that's what made the
  // earlier cached-reference version of this helper flaky: Cypress
  // detected the click's own side effect detaching the element it was
  // still verifying actionability against).
  cy.contains(libraryNodeSelector, libraryMatcher, {
    timeout: longerCommandTimeout
  }).within(() => {
    cy.contains('.clr-treenode-link', tablename, {
      matchCase: false,
      timeout: longerCommandTimeout
    }).click()
  })
}

const setFilterWithValue = (
  variableValue: string,
  valueString: string,
  valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
  callback?: any
) => {
  cy.wait(600)

  cy.focused().type(variableValue)
  cy.wait(100)
  // cy.focused().trigger('input')
  cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
    .first()
    .trigger('keydown', { key: 'ArrowDown' })
  cy.get('.variable-col .autocomplete-wrapper', {
    withinSubject: null
  }).trigger('keydown', { key: 'Enter' })
  cy.focused().tab()
  cy.wait(100)

  if (valueField === 'in') {
    cy.focused().select(valueField.toUpperCase()).trigger('change')
  } else if (valueField === 'between') {
    cy.focused().select(valueField.toUpperCase()).trigger('change')
  } else {
    cy.focused().tab()
    cy.wait(100)
  }

  switch (valueField) {
    case 'value': {
      cy.focused().type(valueString)

      break
    }
    case 'time': {
      cy.focused().type(valueString)

      break
    }
    case 'date': {
      cy.focused().type(valueString)
      cy.focused().tab()

      break
    }
    case 'datetime': {
      const date = valueString.split(' ')[0]
      const time = valueString.split(' ')[1]

      cy.focused().type(date)
      cy.focused().tab()
      cy.focused().tab()
      cy.focused().type(time)

      break
    }
    case 'in': {
      cy.get('.checkbox-vals').then(() => {
        cy.focused().tab()
        cy.focused().click()
        cy.get('.no-values')
          .should('not.exist')
          .then(() => {
            cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
              inputs[0].click()
              cy.get('.in-values-modal .modal-footer button').click()

              cy.get('.modal-footer .btn-success-outline').click()

              if (callback) callback()
            })
          })
      })

      break
    }
    case 'between': {
      cy.focused().tab()

      const start = valueString.split('-')[0]
      const end = valueString.split('-')[1]

      cy.focused().type(start)
      cy.focused().tab()
      cy.focused().type(end)
    }
    default: {
      break
    }
  }

  cy.wait(100)
  cy.focused().tab()
  cy.wait(100)
  cy.focused().tab()
  cy.wait(100)
  cy.focused().tab()
  cy.wait(100)
  cy.focused().tab()
  cy.wait(100)
  cy.focused().click()

  if (callback) callback()
}

const openTableFromViewboxTree = (
  libNameIncludes: string,
  tablenames: string[]
) => {
  cy.get('.add-new clr-tree > clr-tree-node', {
    timeout: longerCommandTimeout
  }).then((treeNodes: any) => {
    let viyaLib

    for (let node of treeNodes) {
      if (node.innerText.toLowerCase().includes(libNameIncludes)) {
        viyaLib = node
        break
      }
    }

    cy.get(viyaLib).within(() => {
      cy.get('p')
        .click()
        .then(() => {
          cy.get('.clr-treenode-link').then(async (innerNodes: any) => {
            for (let innerNode of innerNodes) {
              for (let tablename of tablenames) {
                await pause(300)
                if (innerNode.innerText.toLowerCase().includes(tablename)) {
                  innerNode.click()
                }
              }
            }
          })
        })
    })
  })
}

const pause = (ms: number) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(null)
    }, ms)
  })
}

const visitPage = (url: string) => {
  cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
