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}`)
const fixturePath = 'excels_general/'

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

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

    visitPage('home')
  })

  it('1 | Submits duplicate primary keys', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')

    attachExcelFile('MPE_DATADICTIONARY_duplicate_keys.xlsx', () => {
      clickOnUploadPreview(() => {
        confirmEditPreviewFile(() => {
          submitTable(() => {
            cy.get('.modal-body').then((modalBody: any) => {
              if (modalBody[0].innerText.includes(`Duplicates found:`)) {
                done()
              }
            })
          })
        })
      })
    })
  })

  it('2 | Submits null cells which must not be null', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        cy.get('.ht_master tbody tr').then((rows: any) => {
          cy.get(rows[1].childNodes[2])
            .dblclick({ force: true })
            .then(() => {
              cy.focused()
                .clear()
                .type('{enter}')
                .then(() => {
                  submitTable(() => {
                    cy.get('.modal-body').then((modalBody: any) => {
                      if (
                        modalBody[0].innerHTML
                          .toLowerCase()
                          .includes(`invalid values are present`)
                      ) {
                        done()
                      }
                    })
                  })
                })
            })
        })
      })
    })
  })

  it('3 | Gets basic dynamic cell validation', () => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        cy.get('.ht_master tbody tr').then((rows: any) => {
          cy.get(rows[1].childNodes[5])
            .click({ force: true })
            .then(($td) => {
              cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
                'exist'
              )
            })
        })
      })
    })
  })

  it('4 | Gets advanced dynamic cell validation', () => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_tables')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        cy.get('.ht_master tbody tr').then((rows: any) => {
          cy.get(rows[1].childNodes[3])
            .click({ force: true })
            .then(($td) => {
              cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
                'exist'
              )
              cy.get('.htAutocompleteArrow', {
                withinSubject: rows[1].childNodes[7]
              }).should('exist')
              cy.get('.htAutocompleteArrow', {
                withinSubject: rows[1].childNodes[8]
              }).should('exist')
            })
        })
      })
    })
  })

  // An invalid cell shouldn't block submission if its row
  // is marked for delete
  it('5 | Submits a delete-marked row with an invalid cell', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        cy.get('.ht_master tbody tr').then((rows: any) => {
          // childNodes[5] = SOME_NUM (numeric) — feed it a non-numeric value
          // to trigger HOT's native invalid mark.
          cy.get(rows[1].childNodes[5])
            .dblclick({ force: true })
            .then(() => {
              cy.focused()
                .clear()
                .type('not a number{enter}')
                .then(() => {
                  // childNodes[1] = _____DELETE__THIS__RECORD_____
                  setDeleteFlag(rows[1].childNodes[1], 'Yes', () => {
                    submitTable(() => {
                      // Validation passed despite the invalid SOME_NUM cell:
                      // the confirm-submit modal's Submit button is enabled
                      // (validationDone === 1), not the "invalid values" abort.
                      cy.get('#submitBtn', {
                        timeout: longerCommandTimeout
                      })
                        .should('exist')
                        .should('not.be.disabled')
                        .then(() => done())
                    })
                  })
                })
            })
        })
      })
    })
  })

  // The delete exemption must not affect the uniqueness (primary key) check.
  it('6 | Still blocks a duplicate primary key even on a delete-marked row', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        cy.get('.ht_master tbody tr').then((rows: any) => {
          cy.get(rows[0].childNodes[2]).then((firstPk) => {
            const duplicateValue = firstPk.text()

            // childNodes[2] = PRIMARY_KEY_FIELD
            cy.get(rows[1].childNodes[2])
              .dblclick({ force: true })
              .then(() => {
                cy.focused()
                  .clear()
                  .type(`${duplicateValue}{enter}`)
                  .then(() => {
                    setDeleteFlag(rows[1].childNodes[1], 'Yes', () => {
                      submitTable()

                      cy.get('.duplicate-keys-modal', {
                        timeout: longerCommandTimeout
                      })
                        .should('exist')
                        .then(() => done())
                    })
                  })
              })
          })
        })
      })
    })
  })

  // REGEX_HARD_COL/REGEX_SOFT_COL only exist un-stripped on MPE_X_NEW (see
  // RULE_DEMO_COLS in the getdata.js mock) — MPE_X_TEST stays clean to
  // match the excel fixtures.
  it('7 | Blocks submission of a value that fails HARDREGEX', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        getCellByHeaderAndRow(1, 'REGEX_HARD_COL')
          .dblclick({ force: true })
          .then(() => {
            cy.focused()
              .clear()
              .type('not-an-email{enter}')
              .then(() => {
                getCellByHeaderAndRow(1, 'REGEX_HARD_COL').should(
                  'have.attr',
                  'title',
                  'REGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/'
                )

                submitTable(() => {
                  cy.get('.modal-body').then((modalBody: any) => {
                    if (
                      modalBody[0].innerHTML
                        .toLowerCase()
                        .includes(`invalid values are present`)
                    ) {
                      done()
                    }
                  })
                })
              })
          })
      })
    })
  })

  it('8 | Warns (but still submits) a value that fails SOFTREGEX', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
          .dblclick({ force: true })
          .then(() => {
            cy.focused()
              .clear()
              .type('not a postcode{enter}')
              .then(() => {
                getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
                  .should('have.class', 'dc-warning-cell')
                  .and(
                    'have.attr',
                    'title',
                    'REGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
                  )

                submitTable(() => {
                  // Validation passed despite the SOFTREGEX warning: the
                  // confirm-submit modal's Submit button is enabled
                  // (validationDone === 1), not the "invalid values" abort.
                  cy.get('#submitBtn', { timeout: longerCommandTimeout })
                    .should('exist')
                    .should('not.be.disabled')
                    .then(() => done())
                })
              })
          })
      })
    })
  })

  // Row index 1 (i=1 in the mock's makeRows): SOME_SHORTNUM = (1%99)+1 = 2,
  // SOME_BESTNUM = (1%90)+10 = 11 - FORMULA_HARD_COL/FORMULA_SOFT_COL's
  // rule (SOME_SHORTNUM * SOME_BESTNUM) computes to 22 for this row.
  it('9 | HARDFORMULA shows the computed value and blocks direct edits', () => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        getCellByHeaderAndRow(1, 'FORMULA_HARD_COL').should('have.text', '22')

        getCellByHeaderAndRow(1, 'FORMULA_HARD_COL')
          .dblclick({ force: true })
          .then(() => {
            // readOnly cells don't open an editor - nothing is focused to
            // type into, so this is a no-op if the column is truly readonly.
            cy.focused().type('9999{enter}')

            getCellByHeaderAndRow(1, 'FORMULA_HARD_COL').should(
              'have.text',
              '22'
            )
          })
      })
    })
  })

  it('10 | SOFTFORMULA shows the computed default but accepts an override', () => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL').should('have.text', '22')

        getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL')
          .dblclick({ force: true })
          .then(() => {
            cy.focused().clear().type('override{enter}')

            getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL').should(
              'have.text',
              'override'
            )
          })
      })
    })
  })

  it('11 | Info dropdown shows the applied HARDREGEX/SOFTREGEX pattern', () => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        openColumnDropdown('REGEX_HARD_COL')
        cy.get('.htDropdownMenu').should(($menu) => {
          expect($menu.text()).to.include('REGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/')
        })
        cy.get('body').click(0, 0) // close menu

        openColumnDropdown('REGEX_SOFT_COL')
        cy.get('.htDropdownMenu').should(($menu) => {
          expect($menu.text()).to.include(
            'REGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
          )
        })
      })
    })
  })

  it('12 | Info dropdown labels HARDREGEX/SOFTREGEX separately when a column has both', () => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        openColumnDropdown('REGEX_BOTH_COL')
        cy.get('.htDropdownMenu').should(($menu) => {
          const text = $menu.text()

          expect(text).to.include('HARDREGEX: /^[A-Z0-9_-]+$/')
          expect(text).to.include('SOFTREGEX: /^.{5,10}$/')
        })
      })
    })
  })

  it('13 | REGEX_BOTH_COL: a HARDREGEX failure blocks submission and sets its own tooltip, not yellow', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        // 'bad value' fails HARDREGEX (lowercase + space) but is 9 chars,
        // within SOFTREGEX's 5-10 range - isolates the hard-only failure.
        getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
          .dblclick({ force: true })
          .then(() => {
            cy.focused()
              .clear()
              .type('bad value{enter}')
              .then(() => {
                getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
                  .should('not.have.class', 'dc-warning-cell')
                  .and('have.attr', 'title', 'REGEX: /^[A-Z0-9_-]+$/')

                submitTable(() => {
                  cy.get('.modal-body').then((modalBody: any) => {
                    if (
                      modalBody[0].innerHTML
                        .toLowerCase()
                        .includes(`invalid values are present`)
                    ) {
                      done()
                    }
                  })
                })
              })
          })
      })
    })
  })

  it('14 | REGEX_BOTH_COL: passing HARDREGEX but failing SOFTREGEX warns without blocking', (done) => {
    openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')

    clickOnEdit(() => {
      cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
        timeout: longerCommandTimeout
      }).then(() => {
        scrollGridRight()

        // 'AB' passes HARDREGEX (uppercase only) but fails SOFTREGEX (too
        // short) - previously always inert whenever a column had both rules.
        getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
          .dblclick({ force: true })
          .then(() => {
            cy.focused()
              .clear()
              .type('AB{enter}')
              .then(() => {
                getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
                  .should('have.class', 'dc-warning-cell')
                  .and('have.attr', 'title', 'REGEX: /^.{5,10}$/')

                submitTable(() => {
                  cy.get('#submitBtn', { timeout: longerCommandTimeout })
                    .should('exist')
                    .should('not.be.disabled')
                    .then(() => done())
                })
              })
          })
      })
    })
  })
})

// Handsontable virtualizes columns — with 19 columns on MPE_X_NEW, only
// the ones in the current viewport actually exist in the DOM. Scroll all
// the way right so REGEX_HARD_COL/REGEX_SOFT_COL/FORMULA_HARD_COL/
// FORMULA_SOFT_COL (the last four) render at all. Same technique already
// used in excel.cy.ts. Must be called (and
// re-settle) before any header/body query below, since scrolling replaces
// the previously-rendered column nodes.
const scrollGridRight = () => {
  return cy
    .get('#hotTable')
    .find('div.ht_master.handsontable')
    .find('div.wtHolder')
    .scrollTo('right')
}

// Opens a column header's dropdown menu to reach its `info` item, which
// has a custom renderer showing NAME/LABEL/TYPE/LENGTH/FORMAT and, when the
// column has a HARDREGEX/SOFTREGEX rule, the applied pattern. Clicking the
// header text first selects the column - the renderer reads
// hot.getSelected() to decide which column to describe.
const openColumnDropdown = (headerText: string) => {
  cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
    timeout: longerCommandTimeout
  })
    .parents('th')
    .filter((_, th) => Cypress.$(th).text().includes(headerText))
    .last()
    .as('targetHeader')

  cy.get('@targetHeader').click()
  cy.get('@targetHeader').find('button.changeType').click({ force: true })
}

// Locates a body cell by its column's header text rather than a hardcoded
// childNodes index. Handsontable's hiddenColumns plugin (e.g. HIDDEN_COL,
// used by several demo columns ahead of REGEX_HARD_COL/REGEX_SOFT_COL in
// COLHEADERS) removes a hidden column's cells from BOTH the header row and
// every body row identically, so the header row's th index always lines up
// with the same-position childNode in any body row — this sidesteps having
// to know how many hidden columns precede the target. Re-queries both the
// header and body row fresh each call (rather than accepting a pre-fetched
// row) since virtualization/scrolling can replace either pane's nodes.
//
// The `.should()` retries until headerText actually shows up: Handsontable
// re-renders virtualized columns asynchronously after a scroll event, on
// its own schedule outside Cypress's command queue, so scrollTo() settling
// does not mean the target column has been rendered yet.
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
  return cy
    .get('.ht_clone_top .htCore thead tr th')
    .should(($ths) => {
      const texts = [...$ths].map((th) => th.innerText.trim())
      expect(texts).to.include(headerText)
    })
    .then(($ths) => {
      const index = [...$ths].findIndex(
        (th) => th.innerText.trim() === headerText
      )

      return cy
        .get('.ht_master tbody tr')
        .then((rows: any) => rows[rowIndex].childNodes[index])
        .then((cell) => cy.get(cell))
    })
}

// Opens the _____DELETE__THIS__RECORD_____ dropdown editor on the given cell
// and picks the given choice ('Yes'/'No') — same technique as
// coltype-delete-record.cy.ts (arrow -> autocompleteEditor choice list).
const setDeleteFlag = (
  deleteCell: any,
  value: 'Yes' | 'No',
  callback?: any
) => {
  cy.get(deleteCell)
    .click({ force: true })
    .then(($td) => {
      cy.get('.htAutocompleteArrow', { withinSubject: $td })
        .should('exist')
        .click({ force: true })

      cy.get('.autocompleteEditor .htCore tbody td')
        .contains(value)
        .click({ force: true })
        .then(() => {
          if (callback) callback()
        })
    })
}

const clickOnEdit = (callback?: any) => {
  cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
    .click()
    .then(() => {
      if (callback) callback()
    })
}

const openTableFromTree = (libNameIncludes: string, tablename: string) => {
  cy.get('.app-loading', { timeout: longerCommandTimeout })
    .should('not.exist')
    .then(() => {
      cy.get('.nav-tree 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('.clr-tree-node-content-container > button').click()

          cy.get('.clr-treenode-link').then((innerNodes: any) => {
            for (let innerNode of innerNodes) {
              if (innerNode.innerText.toLowerCase().includes(tablename)) {
                innerNode.click()
                break
              }
            }
          })
        })
      })
    })
}

const attachExcelFile = (excelFilename: string, callback?: any) => {
  cy.get('.buttonBar button:last-child')
    .click()
    .then(() => {
      cy.get('input[type="file"]#file-upload')
        .attachFile(`/${fixturePath}/${excelFilename}`)
        .then(() => {
          cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
            modalBtn.click()
            if (callback) callback()
          })
        })
    })
}

const clickOnUploadPreview = (callback?: any) => {
  cy.get('.buttonBar button.btn-primary.btn-upload-preview')
    .click()
    .then(() => {
      if (callback) callback()
    })
}

const confirmEditPreviewFile = (callback?: any) => {
  cy.get('.modal-footer button.btn-success-outline')
    .click()
    .then(() => {
      if (callback) callback()
    })
}

const submitTable = (callback?: any) => {
  cy.get('.btnCtrl button.btn-primary')
    .click()
    .then(() => {
      if (callback) callback()
    })
}

const submitTableMessage = (callback?: any) => {
  cy.get('.modal-footer .btn.btn-sm.btn-success-outline')
    .click()
    .then(() => {
      if (callback) callback()
    })
}

const submitExcel = (callback?: any) => {
  cy.get('.buttonBar button.preview-submit')
    .click()
    .then(() => {
      if (callback) callback()
    })
}

const rejectExcel = (callback?: any) => {
  cy.get('button', { timeout: longerCommandTimeout })
    .should('contain', 'Approve')
    .then((allButtons: any) => {
      for (let approvalButton of allButtons) {
        if (approvalButton.innerText.toLowerCase().includes('approve')) {
          approvalButton.click()
          break
        }
      }

      cy.get('button.btn-danger')
        .should('exist')
        .should('not.be.disabled')
        .click()
        .then(() => {
          cy.get('.modal-footer button.btn-success-outline')
            .click()
            .then(() => {
              cy.get('app-history')
                .should('exist')
                .then(() => {
                  if (callback) callback()
                })
            })
        })
    })
}

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