import { html, fixture, assert, oneEvent, nextFrame, aTimeout } from '@open-wc/testing'
import { spy } from 'sinon'
import {
  EventTypes,
  IOAuth2Authorization,
  IRequestAuthorization,
  RequestAuthorizationKind,
} from '@api-client/core/browser.js'
import type OAuth2Authorization from '../../../src/elements/authorization/ui/OAuth2Authorization.js'
import type Input from '../../../src/md/input/Input.js'
import type UiButton from '../../../src/md/button/internals/button.js'
import '../../../src/define/authorization/oauth2-authorization.js'

describe('Authorization', () => {
  describe('<oauth2-authorization>', () => {
    describe('implicit', () => {
      const baseConfig: IOAuth2Authorization = {
        grantType: 'implicit',
        redirectUri: 'https://redirect.com/',
        clientId: 'test-client-id',
        authorizationUri: 'https://accounts.google.com/o/oauth2/v2/auth',
        scopes: ['email', 'profile'],
      }

      async function basicFixture(opts: IOAuth2Authorization = {}): Promise<OAuth2Authorization> {
        const auth: IRequestAuthorization = {
          config: opts,
          enabled: true,
          kind: RequestAuthorizationKind,
          type: 'oauth 2',
          valid: true,
        }
        return fixture(html`<oauth2-authorization .authorization="${auth}"></oauth2-authorization>`)
      }

      async function baseUriFixture(baseUri: string, opts: IOAuth2Authorization): Promise<OAuth2Authorization> {
        const auth: IRequestAuthorization = {
          config: opts,
          enabled: true,
          kind: RequestAuthorizationKind,
          type: 'oauth 2',
          valid: true,
        }
        return fixture(
          html`<oauth2-authorization .authorization="${auth}" .baseUri="${baseUri}"></oauth2-authorization>`
        )
      }

      describe('DOM rendering', () => {
        let element: OAuth2Authorization
        let form: HTMLFormElement
        beforeEach(async () => {
          element = await basicFixture({ ...baseConfig })
          form = element.shadowRoot!.querySelector('form')!
        })

        it('has form in the DOM', async () => {
          assert.ok(form)
        })

        it('has form has autocomplete', async () => {
          assert.equal(form.getAttribute('autocomplete'), 'on')
        })

        it(`has the clientId input`, async () => {
          const input = form.querySelector(`[name="clientId"]`) as Input
          assert.ok(input, 'has the input')
        })

        it(`has the authorizationUri input`, async () => {
          const input = form.querySelector(`[name="authorizationUri"]`) as Input
          assert.ok(input, 'has the input')
        })

        it('renders the redirect URI field', async () => {
          const section = element.shadowRoot!.querySelector('[name="redirectUri"]')
          assert.ok(section)
        })

        it('does not render token section when no token information', () => {
          const node = element.shadowRoot!.querySelector('.current-token')
          assert.notOk(node)
        })

        it('renders token section when token information is set', async () => {
          element.config.accessToken = 'test-token'
          element.requestUpdate()
          await element.updateComplete
          const node = element.shadowRoot!.querySelector('.current-token')!
          assert.ok(node)
          const label = node.querySelector('.token-value')!
          assert.equal(label.textContent!.trim(), 'test-token')
        })

        it('does not render the PKCE checkbox', async () => {
          const node = element.shadowRoot!.querySelector('[name="pkce"]')
          assert.notOk(node)
        })
      })

      describe('Data initialization', () => {
        let element: OAuth2Authorization
        beforeEach(async () => {
          element = await basicFixture({ ...baseConfig })
        })

        it(`has the clientId`, async () => {
          const input = element.shadowRoot!.querySelector(`[name="clientId"]`) as Input
          assert.equal(input.value, baseConfig.clientId)
        })

        it(`has the authorizationUri input`, async () => {
          const input = element.shadowRoot!.querySelector(`*[name="authorizationUri"]`) as Input
          assert.equal(input.value, baseConfig.authorizationUri)
        })
      })

      describe('Change notification', () => {
        let element: OAuth2Authorization
        beforeEach(async () => {
          element = await basicFixture({ ...baseConfig })
        })

        it(`notifies when clientId changes`, async () => {
          setTimeout(() => {
            const input = element.shadowRoot!.querySelector(`[name="clientId"]`) as Input
            input.value = 'changed'
            input.dispatchEvent(new CustomEvent('change'))
          })
          const e = await oneEvent(element, 'change')
          assert.ok(e)
        })

        it(`notifies when authorizationUri changes`, async () => {
          setTimeout(() => {
            const input = element.shadowRoot!.querySelector(`[name="authorizationUri"]`) as Input
            input.value = 'changed'
            input.dispatchEvent(new CustomEvent('change'))
          })
          const e = await oneEvent(element, 'change')
          assert.ok(e)
        })
      })

      describe('Data serialization', () => {
        let element: OAuth2Authorization
        beforeEach(async () => {
          element = await basicFixture({ ...baseConfig })
        })

        it(`serializes the clientId`, async () => {
          const result = element.serialize()
          assert.equal(result.clientId, baseConfig.clientId)
        })

        it(`serializes the authorizationUri`, async () => {
          const result = element.serialize()
          assert.equal(result.authorizationUri, baseConfig.authorizationUri)
        })

        it(`serializes the redirectUri`, async () => {
          const result = element.serialize()
          assert.equal(result.redirectUri, baseConfig.redirectUri)
        })

        it('has no pkce value', () => {
          element.config.pkce = true
          const result = element.serialize()
          assert.isUndefined(result.pkce)
        })
      })

      describe('authorization event', () => {
        let element: OAuth2Authorization

        beforeEach(async () => {
          element = await basicFixture({ ...baseConfig })
        })

        async function sendResponse(
          state?: string,
          tokenType?: string,
          e?: CustomEvent<IOAuth2Authorization>
        ): Promise<IOAuth2Authorization> {
          await aTimeout(1)
          return {
            accessToken: 'test-token',
            tokenType,
            state: state || e!.detail.state,
          }
        }

        function mockTokenRequest(state?: string, tokenType?: string): void {
          window.addEventListener(EventTypes.Authorization.OAuth2.authorize, function f(e) {
            window.removeEventListener(EventTypes.Authorization.OAuth2.authorize, f)
            e.preventDefault()
            const typed = e as CustomEvent<IOAuth2Authorization & { result?: unknown }>
            typed.detail.result = sendResponse(state, tokenType, typed)
          })
        }

        function mockTokenErrorRequest(message?: string): void {
          window.addEventListener(EventTypes.Authorization.OAuth2.authorize, function f(e) {
            window.removeEventListener(EventTypes.Authorization.OAuth2.authorize, f)
            e.preventDefault()
            const typed = e as CustomEvent<IOAuth2Authorization & { result?: unknown }>
            typed.detail.result = Promise.reject(new Error(message))
          })
        }

        it(`sets the serialized values onto the event`, async () => {
          const handler = spy()
          element.addEventListener(EventTypes.Authorization.OAuth2.authorize, handler)
          element.authorize()
          const { detail } = handler.args[0][0] as CustomEvent<IOAuth2Authorization & { result?: unknown }>
          delete detail.state
          delete detail.result
          assert.deepEqual(detail, element.serialize())
        })

        it('calls authorize() from button click', () => {
          const button = element.shadowRoot!.querySelector('.auth-button') as UiButton
          const handler = spy()
          element.addEventListener(EventTypes.Authorization.OAuth2.authorize, handler)
          button.click()
          assert.isTrue(handler.called)
        })

        it('sets the #authorizing flag', async () => {
          mockTokenRequest()
          const button = element.shadowRoot!.querySelector('.auth-button') as UiButton
          button.click()
          assert.isTrue(element.authorizing)
        })

        it('ignores authorization when did not pass validation', async () => {
          element.config.clientId = ''
          element.requestUpdate()
          await element.updateComplete
          element.authorize()
          assert.isFalse(element.authorizing)
        })

        it('resets the #authorizing flag when token response', async () => {
          mockTokenRequest()
          element.authorize()
          await aTimeout(2)
          await nextFrame()
          assert.isFalse(element.authorizing)
        })

        it('sets state on the event', async () => {
          const handler = spy()
          element.addEventListener(EventTypes.Authorization.OAuth2.authorize, handler)
          element.authorize()
          const { detail } = handler.args[0][0] as CustomEvent<IOAuth2Authorization & { result?: unknown }>
          const eventState = detail.state
          assert.typeOf(eventState, 'string', 'event state is set')
        })

        it('sets values from the response event with state', async () => {
          mockTokenRequest(undefined, 'other')
          element.authorize()
          await aTimeout(2)
          await nextFrame()
          assert.equal(element.config.accessToken, 'test-token')
          assert.equal(element.config.tokenType, 'other')
        })

        it('ignores events with different state', async () => {
          mockTokenRequest('unknown-state', 'other')
          element.authorize()
          await aTimeout(2)
          await nextFrame()
          assert.isUndefined(element.config.accessToken)
        })

        it('ignores the response event when token is already set', async () => {
          mockTokenRequest(undefined, 'token-value')
          element.authorize()
          element.config.accessToken = 'test-token'
          await aTimeout(2)
          await nextFrame()
          assert.equal(element.config.accessToken, 'test-token')
        })

        it('restores default token type from the response event', async () => {
          mockTokenRequest()
          element.authorize()
          element.config.tokenType = 'custom'
          element.requestUpdate()
          await element.updateComplete
          await aTimeout(2)
          await nextFrame()
          assert.equal(element.config.tokenType, 'Bearer')
        })

        it('dispatches change event when the token is received', async () => {
          mockTokenRequest()
          const handler = spy()
          element.addEventListener('change', handler)
          element.authorize()
          await aTimeout(2)
          await nextFrame()
          assert.isTrue(handler.called)
        })

        it('resets the #authorizing flag when token error', async () => {
          mockTokenErrorRequest()
          try {
            await element.authorize()
          } catch {
            // ..
          }
          await nextFrame()
          assert.isFalse(element.authorizing)
        })

        it('sets "lastErrorMessage" with event message', async () => {
          const message = 'Test error'
          mockTokenErrorRequest(message)
          await nextFrame()
          try {
            await element.authorize()
          } catch {
            // ..
          }
          await nextFrame()
          assert.equal(element.lastErrorMessage, message)
        })

        it('renders error message', async () => {
          mockTokenErrorRequest('This is an error')
          try {
            await element.authorize()
          } catch {
            // ...
          }
          await nextFrame()
          const node = element.shadowRoot!.querySelector('.error-message')
          assert.ok(node)
        })

        it('clears "lastErrorMessage" when requesting the token again', async () => {
          mockTokenErrorRequest('This is an error')
          try {
            await element.authorize()
          } catch {
            // ...
          }
          await nextFrame()
          await element.authorize()
          assert.isUndefined(element.lastErrorMessage)
        })
      })

      describe('#baseUri', () => {
        const baseUri = 'https://api.domain.com/auth'

        it('adds base URI to authorizationUri', async () => {
          const params = { ...baseConfig }
          params.authorizationUri = '/authorize'
          const element = await baseUriFixture(baseUri, params)
          const result = element.serialize()
          assert.equal(result.authorizationUri, 'https://api.domain.com/auth/authorize')
        })

        it('adds base URI to redirectUri', async () => {
          const params = { ...baseConfig }
          params.redirectUri = '/redirect'

          const element = await baseUriFixture(baseUri, params)
          const result = element.serialize()
          assert.equal(result.redirectUri, 'https://api.domain.com/auth/redirect')
        })

        it('ignores trailing slash', async () => {
          const uri = `${baseUri}/`
          const params = { ...baseConfig }
          params.redirectUri = '/redirect'

          const element = await baseUriFixture(uri, params)
          const result = element.serialize()
          assert.equal(result.redirectUri, 'https://api.domain.com/auth/redirect')
        })

        it('makes authorizationUri input text type', async () => {
          const element = await baseUriFixture(baseUri, { ...baseConfig })
          const node = element.shadowRoot!.querySelector('[name="authorizationUri"]') as Input
          assert.equal(node.type, 'string')
        })
      })

      describe('a11y', () => {
        let element: OAuth2Authorization

        beforeEach(async () => {
          element = await basicFixture({ ...baseConfig })
        })

        it('passes automated a11y tests', async () => {
          await assert.isAccessible(element)
        })
      })
    })
  })
})
