import { nextFrame, fixture, expect, assert, html, oneEvent } from '@open-wc/testing'
import sinon from 'sinon'
import { MarkedHighlightElement } from '../../../src/index.js'
import '../../../src/elements/highlight/marked-highlight.js'
import { Tokens } from 'marked'

describe('MarkedHighlightElement', () => {
  async function basicFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown">
          # Test
        </script>
      </marked-highlight>`
    )
  }

  async function propertyMarkdownFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight markdown="# Test">
        <div id="output" slot="markdown-html"></div>
      </marked-highlight>`
    )
  }

  async function noContentFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown"></script>
      </marked-highlight>`
    )
  }

  async function camelCaseHTMLFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown">
          \`\`\`html
          <div camelCase></div>
          \`\`\`
        </script>
      </marked-highlight>`
    )
  }

  // async function camelCaseHTMLWithoutChildFixture(): Promise<MarkedHighlightElement> {
  //   return fixture(
  //     html`<marked-highlight>
  //       <div id="output" slot="markdown-html"></div>
  //       <script type="text/markdown">
  //         \`\`\`html
  //         <div camelCase></div>
  //         \`\`\`
  //       </script>
  //     </marked-highlight>`
  //   )
  // }

  // async function badHTMLWithoutChildFixture(): Promise<MarkedHighlightElement> {
  //   return fixture(
  //     html`<marked-highlight>
  //       <div id="output" slot="markdown-html"></div>
  //       <script type="text/markdown">
  //         \`\`\`html
  //         <p><div></p></div>
  //         \`\`\`
  //       </script>
  //     </marked-highlight>`
  //   )
  // }

  async function rendererFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown">
          [Link](http://url.com)
        </script>
      </marked-highlight>`
    )
  }

  // async function sanitizerFixture(): Promise<MarkedHighlightElement> {
  //   return fixture(
  //     html`<marked-highlight sanitize>
  //       <div id="output" slot="markdown-html"></div>
  //       <script type="text/markdown">
  //         [Link](http://url.com" onclick="alert(1)")
  //         <a href="http://url.com" onclick="alert(1)">Link</a>

  //         \`\`\`html
  //         <a href="http://url.com" onclick="alert(1)">Link</a>
  //         \`\`\`
  //       </script>
  //     </marked-highlight>`
  //   )
  // }

  async function remoteContentFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown" src="test/elements/highlight/test.md">
          # Loading
          Please wait...
        </script>
      </marked-highlight>`
    )
  }

  async function badRemoteContentFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown" src="test/elements/highlight/test3.md"></script>
      </marked-highlight>`
    )
  }

  async function sanitizedRemoteContentFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown" src="test/elements/highlight/remoteSanitization.md"></script>
      </marked-highlight>`
    )
  }

  async function unsanitizedRemoteContentFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight disableRemoteSanitization>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown" src="test/elements/highlight/remoteSanitization.md"></script>
      </marked-highlight>`
    )
  }

  async function markedOptionsFixture(): Promise<MarkedHighlightElement> {
    return fixture(
      html`<marked-highlight sanitize pedantic breaks>
        <div id="output" slot="markdown-html"></div>
        <script type="text/markdown" src="test/elements/highlight/remoteSanitization.md"></script>
      </marked-highlight>`
    )
  }

  // Thanks IE10.
  function isHidden(element: HTMLElement): boolean {
    const rect = element.getBoundingClientRect()
    return rect.width === 0 && rect.height === 0
  }

  // // Replace reserved HTML characters with their character entity equivalents to
  // // match the transform done by Markdown.
  // //
  // // The Marked library itself is not used because it wraps code blocks in
  // // `<code><pre>`, which is superfluous for testing purposes.
  // function escapeHTML(string) {
  //   const span = document.createElement('span');
  //   span.textContent = string;
  //   return span.innerHTML;
  // }

  describe('Property setters', () => {
    let element: MarkedHighlightElement
    beforeEach(async () => {
      element = await basicFixture()
    })

    it(`triggers the renderMarkdown() when setting the markdown property`, async () => {
      const spy = sinon.spy(element, 'renderMarkdown')
      element.markdown = 'test'
      await nextFrame()
      assert.isTrue(spy.called)
    })

    it(`ignores renderMarkdown() when setting the same value for the markdown property`, async () => {
      element.markdown = 'test'
      await nextFrame()
      const spy = sinon.spy(element, 'renderMarkdown')
      element.markdown = 'test'
      await nextFrame()
      assert.isFalse(spy.called)
    })

    it(`triggers the renderMarkdown() when setting the breaks property`, async () => {
      const spy = sinon.spy(element, 'renderMarkdown')
      element.breaks = true
      await nextFrame()
      assert.isTrue(spy.called)
    })

    it(`ignores renderMarkdown() when setting the same value for the breaks property`, async () => {
      element.breaks = true
      await nextFrame()
      const spy = sinon.spy(element, 'renderMarkdown')
      element.breaks = true
      await nextFrame()
      assert.isFalse(spy.called)
    })

    it(`triggers the renderMarkdown() when setting the pedantic property`, async () => {
      const spy = sinon.spy(element, 'renderMarkdown')
      element.pedantic = true
      await nextFrame()
      assert.isTrue(spy.called)
    })

    it(`ignores renderMarkdown() when setting the same value for the pedantic property`, async () => {
      element.pedantic = true
      await nextFrame()
      const spy = sinon.spy(element, 'renderMarkdown')
      element.pedantic = true
      await nextFrame()
      assert.isFalse(spy.called)
    })

    it(`triggers the renderMarkdown() when setting the sanitize property`, async () => {
      const spy = sinon.spy(element, 'renderMarkdown')
      element.sanitize = true
      await nextFrame()
      assert.isTrue(spy.called)
    })

    it(`ignores renderMarkdown() when setting the same value for the sanitize property`, async () => {
      element.sanitize = true
      await nextFrame()
      const spy = sinon.spy(element, 'renderMarkdown')
      element.sanitize = true
      await nextFrame()
      assert.isFalse(spy.called)
    })

    it(`triggers the renderMarkdown() when setting the renderer property`, async () => {
      const spy = sinon.spy(element, 'renderMarkdown')
      element.renderer = (): void => {
        /* */
      }
      await nextFrame()
      assert.isTrue(spy.called)
    })

    it(`triggers the renderMarkdown() when setting the sanitizer property`, async () => {
      const spy = sinon.spy(element, 'renderMarkdown')
      element.sanitizer = (): void => {
        /* */
      }
      await nextFrame()
      assert.isTrue(spy.called)
    })
  })

  describe('renders markdown from property setter', () => {
    let outputElement: HTMLDivElement
    beforeEach(async () => {
      await propertyMarkdownFixture()
      outputElement = document.getElementById('output') as HTMLDivElement
    })

    it('renders the code', () => {
      assert.equal(outputElement.innerHTML, '<h1>Test</h1>\n')
    })
  })

  describe('has options of marked library', () => {
    let markedElement: MarkedHighlightElement
    beforeEach(async () => {
      markedElement = await markedOptionsFixture()
    })

    it('has sanitize', () => {
      expect(markedElement.sanitize).to.equal(true)
    })

    it('has sanitizer', () => {
      expect(markedElement.sanitizer).to.equal(undefined)
    })

    it('has pedantic', () => {
      expect(markedElement.pedantic).to.equal(true)
    })

    it('has breaks', () => {
      expect(markedElement.breaks).to.equal(true)
    })
  })

  describe('with .markdown-html child', () => {
    describe('ignores no content', () => {
      let markedElement: MarkedHighlightElement
      let outputElement: HTMLDivElement
      let proofElement: HTMLDivElement
      beforeEach(async () => {
        markedElement = await noContentFixture()
        proofElement = document.createElement('div')
        outputElement = document.getElementById('output') as HTMLDivElement
      })

      it('in code blocks', () => {
        proofElement.innerHTML = ''
        assert.equal(outputElement, markedElement.outputElement)
        assert.isTrue(isHidden(markedElement.shadowRoot!.querySelector('#content')!))
        assert.equal(markedElement.markdown, undefined)
        assert.equal(outputElement.innerHTML, proofElement.innerHTML)
      })
    })
  })

  describe('with custom renderer', () => {
    let markedElement: MarkedHighlightElement
    let outputElement: HTMLDivElement
    let proofElement: HTMLDivElement

    beforeEach(async () => {
      markedElement = await rendererFixture()
      outputElement = document.getElementById('output') as HTMLDivElement
      proofElement = document.createElement('div')
    })

    it('takes custom link renderer', async () => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      markedElement.renderer = (renderer: any): void => {
        renderer.link = (link: Tokens.Link): string => {
          return `<a href="${link.href}" target="_blank">${link.text}</a>`
        }
      }
      await nextFrame()
      proofElement.innerHTML = '<a href="http://url.com" target="_blank">Link</a>'
      expect(outputElement.innerHTML).to.include(proofElement.innerHTML)
    })
  })

  describe('with remote content', () => {
    let markedElement: MarkedHighlightElement
    let outputElement: HTMLDivElement
    let proofElement: HTMLDivElement

    describe('successful fetch', () => {
      beforeEach(async () => {
        markedElement = await remoteContentFixture()
        outputElement = document.getElementById('output') as HTMLDivElement
        proofElement = document.createElement('div')
      })

      it('renders remote content', async () => {
        proofElement.innerHTML = '<h1>Test</h1>\n<p><a href="http://url.com/">Link</a></p>\n'
        await oneEvent(markedElement, 'markedloaded')
        await nextFrame()
        expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
      })

      it('renders content while remote content is loading', () => {
        proofElement.innerHTML = '<h1>Loading</h1>\n<p>Please wait...</p>\n'
        expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
      })

      it('renders new remote content when src changes', async () => {
        await oneEvent(markedElement, 'markedloaded')
        await nextFrame()
        proofElement.innerHTML = '<h1>Test 2</h1>\n'
        // @ts-expect-error for testing
        markedElement.querySelector('[type="text/markdown"]').src = 'test/elements/highlight/test2.md'
        await oneEvent(markedElement, 'markedloaded')
        await nextFrame()
        expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
      })
    })

    describe('fails to load', () => {
      beforeEach(async () => {
        markedElement = await badRemoteContentFixture()
        outputElement = document.getElementById('output') as HTMLDivElement
        proofElement = document.createElement('div')
      })

      it('renders error message', async () => {
        proofElement.innerHTML = '<p>Failed loading markdown source</p>\n'
        await oneEvent(markedElement, 'markedloaderror')
        await nextFrame()
        expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
      })

      it("doesn't render error message when default is prevented", async () => {
        proofElement.innerHTML = ''
        markedElement.addEventListener('markedloaderror', (e) => {
          e.preventDefault()
        })
        await oneEvent(markedElement, 'markedloaderror')
        await nextFrame()
        expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
      })
    })

    describe('sanitizing remote content', () => {
      describe('sanitized', () => {
        beforeEach(async () => {
          markedElement = await sanitizedRemoteContentFixture()
        })

        it('sanitizes remote content', async () => {
          outputElement = markedElement.querySelector('#output')!
          proofElement = document.createElement('div')
          proofElement.innerHTML = '<div></div>\n'
          await oneEvent(markedElement, 'markedloaded')
          await nextFrame()
          assert.isTrue(markedElement.sanitize)
          assert.isNotTrue(markedElement.disableRemoteSanitization)
          expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
        })
      })

      describe('unsanitized', () => {
        beforeEach(async () => {
          markedElement = await unsanitizedRemoteContentFixture()
        })

        it('Does not sanitize remote content', async () => {
          outputElement = markedElement.querySelector('#output')!
          proofElement = document.createElement('div')
          proofElement.innerHTML = '<div></div>\n'
          await oneEvent(markedElement, 'markedloaded')
          await nextFrame()

          assert.isNotTrue(markedElement.sanitize)
          assert.isTrue(markedElement.disableRemoteSanitization)
          expect(outputElement.innerHTML).to.equal(proofElement.innerHTML)
        })
      })
    })
  })

  describe('events', () => {
    let markedElement: MarkedHighlightElement
    let outputElement: HTMLDivElement
    beforeEach(async () => {
      markedElement = await camelCaseHTMLFixture()
      outputElement = document.getElementById('output') as HTMLDivElement
    })

    it('render() fires markedrendercomplete', async () => {
      setTimeout(() => {
        markedElement.renderMarkdown()
      })
      await oneEvent(markedElement, 'markedrendercomplete')
      await nextFrame()
      expect(outputElement.innerHTML).to.not.equal('')
    })
  })
})
