# frozen_string_literal: true

require 'fileutils'
require 'net/http'
require 'securerandom'

##
# Test for a preview that is usually deployed in Elastic Apps. It previews all
# branches of the `--target_repo`. The test runs everything in the defined
# order because starting the preview is fairly heavy and the preview is
# designed to update itself as its target repo changes so we start it once and
# play with the target repo during the tests.
RSpec.describe 'previewing built docs', order: :defined do
  very_large_text = 'muchtext' * 1024 * 1024 * 5 # 40mb
  repo_root = File.expand_path '../../', __dir__
  readme_resources = "#{repo_root}/resources/readme"

  convert_before do |src, dest|
    repo = src.repo_with_index 'repo', <<~ASCIIDOC
      Some text.

      image::resources/readme/cat.jpg[A cat]
      image::resources/readme/example.svg[An example svg]
      image::resources/very_large.jpg[Not a jpg but very big]
    ASCIIDOC
    repo.cp "#{readme_resources}/cat.jpg", 'resources/readme/cat.jpg'
    repo.cp "#{readme_resources}/example.svg", 'resources/readme/example.svg'
    repo.write 'resources/very_large.jpg', very_large_text
    repo.commit 'add images'
    book = src.book 'Test'
    book.source repo, 'index.asciidoc'
    book.source repo, 'resources'
    dest.prepare_convert_all(src.conf).convert
  end
  before(:context) do
    @preview = @dest.start_preview
  end
  after(:context) do
    @preview&.exit
  end
  let(:repo) { @dest.bare_repo.sub '.git', '' }
  let(:preview) { @preview }
  let(:logs) { preview.logs }

  # The preview server reads the branch from the `Host` header. It throws out
  # everything after and including the first `.` so you can hit a branch
  # at urls like `http://master.docs-preview.app.elstc.co/`. Also, if it
  # can't find the `.` then it just assumes master.
  let(:host) { "#{branch}.localhost" }

  def wait_for_logs(regexp, timeout: 10)
    preview.wait_for_logs(regexp, timeout)
  rescue Timeout::Error
    expect(preview.logs).to match(regexp)
  end

  def wait_for_access(path)
    wait_for_logs(/^#{watermark} #{host}.+#{path}.+$/)
  end

  def get(watermark, branch, path)
    uri = URI("http://localhost:8000/#{path}")
    req = Net::HTTP::Get.new(uri)
    raise "branches can't contain [.]" if branch.include? '.'

    req['X-Opaque-Id'] = watermark
    req['Host'] = host
    Net::HTTP.start(uri.hostname, uri.port, read_timeout: 20) do |http|
      http.request(req)
    end
  end

  shared_context 'docs for branch' do
    watermark = SecureRandom.uuid
    let(:watermark) { watermark }
    let(:current_url) { 'guide/test/current' }
    let(:liveness) { get watermark, branch, 'liveness' }
    let(:diff) { get watermark, branch, 'diff' }
    let(:robots_txt) { get watermark, branch, 'robots.txt' }
    let(:root) { get watermark, branch, '' }
    let(:guide_root) { get watermark, branch, 'guide/index.html' }
    let(:current_index) { get watermark, branch, "#{current_url}/index.html" }
    let(:current_toc) { get watermark, branch, "#{current_url}/toc.html" }
    let(:cat_image) do
      get watermark, branch, "#{current_url}/resources/readme/cat.jpg"
    end
    let(:svg_image) do
      get watermark, branch, "#{current_url}/resources/readme/example.svg"
    end
    let(:very_large) do
      get watermark, branch, "#{current_url}/resources/very_large.jpg"
    end
    let(:directory) do
      get watermark, branch, 'guide'
    end
    let(:legacy_redirect) do
      get watermark, branch, 'guide/reference/setup/'
    end
    let(:outside_of_guide) do
      get watermark, branch, 'cloud/elasticsearch-service/signup'
    end
  end

  let(:expected_js_state) { {} }
  let(:expected_language) { 'en' }

  it 'logs that the built docs are ready' do
    wait_for_logs(/Built docs are ready/)
  end
  include_examples 'the favicon'

  shared_examples 'serves some docs' do |supports_gapped: true|
    context 'the liveness check' do
      it '200s' do
        expect(liveness).to serve(include("R'lyeh"))
      end
    end
    context 'the root' do
      it 'redirects to the guide root' do
        expect(root).to redirect_to(eq('/guide/index.html'))
      end
      it 'logs the access to the docs root' do
        wait_for_access '/'
        expect(logs).to include(<<~LOGS)
          #{watermark} #{host} GET / HTTP/1.1 301
        LOGS
      end
    end
    context 'the docs root' do
      it 'contains a link to the current index' do
        expect(guide_root).to serve(doc_body(include(<<~HTML.strip)))
          <a href="test/current/index.html" class="ulink" target="_top">Test</a>
        HTML
      end
      it 'logs access to the docs root' do
        wait_for_access '/'
        expect(logs).to include(<<~LOGS)
          #{watermark} #{host} GET /guide/index.html HTTP/1.1 200
        LOGS
      end
      it 'contains the gtag js' do
        expect(guide_root).to serve(include(<<~HTML.strip))
          https://www.googletagmanager.com/gtag/js
        HTML
      end
      it 'logs the access to the guide root' do
        wait_for_access '/guide/index.html'
        expect(logs).to include(<<~LOGS)
          #{watermark} #{host} GET /guide/index.html HTTP/1.1 200
        LOGS
      end
      if supports_gapped
        context 'when air gapped' do
          let(:host) { "gapped_#{branch}.localhost" }
          it "doesn't contain the gtag js" do
            expect(guide_root).not_to serve(include(<<~HTML.strip))
              https://www.googletagmanager.com/gtag/js
            HTML
          end
          it 'logs the access to the air gapped docs root' do
            wait_for_access '/guide/index.html'
            expect(logs).to include(<<~LOGS)
              #{watermark} #{host} GET /guide/index.html HTTP/1.1 200
            LOGS
          end
        end
      end
    end
    context 'the current index' do
      it 'has the correct initial_js_state' do
        expect(current_index).to serve(initial_js_state(eq(expected_js_state)))
      end
      it 'has the correct language' do
        expect(current_index).to serve(include(<<~HTML.strip))
          <section id="guide" lang="#{expected_language}">
        HTML
      end
    end
    context 'the current table of contents' do
      it "isn't templated" do
        expect(current_toc).to serve(start_with('<div class="toc">'))
      end
    end
    it 'serves a "go away" robots.txt' do
      expect(robots_txt).to serve(eq(<<~TXT))
        User-agent: *
        Disallow: /
      TXT
      expect(robots_txt['Content-Type']).to eq('text/plain')
    end
    context 'for a legacy redirect' do
      it 'serves the redirect' do
        expect(legacy_redirect).to redirect_to(
          eq(
            '/guide/en/elasticsearch/reference/current/setup.html'
          )
        )
      end
      it 'logs the access to the legacy redirect' do
        wait_for_access '/guide/reference/setup/'
        expect(logs).to include(<<~LOGS)
          #{watermark} #{host} GET /guide/reference/setup/ HTTP/1.1 301
        LOGS
      end
    end
    context 'for a url outside of the docs' do
      it 'redirects to the public site' do
        expect(outside_of_guide).to redirect_to(
          eq('https://www.elastic.co/cloud/elasticsearch-service/signup'),
          '302'
        )
      end
      it 'logs the access to the url outside of the docs' do
        wait_for_access '/cloud/elasticsearch-service/signup'
        expect(logs).to include(<<~LOGS)
          #{watermark} #{host} GET /cloud/elasticsearch-service/signup HTTP/1.1 302
        LOGS
      end
      if supports_gapped
        context 'when air gapped' do
          let(:host) { "gapped_#{branch}.localhost" }
          it '404s' do
            expect(outside_of_guide.code).to eq('404')
          end
          it 'logs the access to the url outside of the docs' do
            wait_for_access '/cloud/elasticsearch-service/signup'
            expect(logs).to include(<<~LOGS)
              #{watermark} #{host} GET /cloud/elasticsearch-service/signup HTTP/1.1 404
            LOGS
          end
        end
      end
    end
  end
  shared_examples '404s' do
    context 'the liveness check' do
      it '200s' do
        expect(liveness).to serve(include("R'lyeh"))
      end
    end
    it '404s for the docs root' do
      expect(guide_root.code).to eq('404')
    end
    it 'logs the access to the docs root' do
      wait_for_access '/guide/index.html'
      expect(logs).to include(<<~LOGS)
        #{watermark} #{host} GET /guide/index.html HTTP/1.1 404
      LOGS
    end
    it '404s for the diff' do
      expect(diff.code).to eq('404')
    end
    it 'logs the access to the diff' do
      wait_for_access '/diff'
      expect(logs).to include(<<~LOGS)
        #{watermark} #{host} GET /diff HTTP/1.1 404
      LOGS
    end
  end
  shared_examples 'valid diff' do
    it 'has the html5 doctype' do
      expect(diff).to serve(include('<!DOCTYPE html>'))
    end
    it 'has the branch in the title' do
      expect(diff).to serve(include("<title>Diff for #{branch}</title>"))
    end
    it "doesn't contain a link to the sitemap" do
      expect(diff).not_to serve(include('sitemap.xml'))
    end
    it "doesn't contain a link to the revision file" do
      expect(diff).not_to serve(include('revisions.txt'))
    end
    it "doesn't contain a link to the branch tracker file" do
      expect(diff).not_to serve(include('branches.yaml'))
    end
    it "doesn't warn about unprocesed output" do
      expect(diff).not_to serve(include('Unprocessed results from git'))
    end
    it 'logs access to the diff when it is accessed' do
      wait_for_access '/diff'
      expect(logs).to include(<<~LOGS)
        #{watermark} #{host} GET /diff HTTP/1.1 200
      LOGS
    end
  end

  describe 'for the master branch' do
    let(:branch) { 'master' }
    include_context 'docs for branch'
    include_examples 'serves some docs'
    context 'for a JPG' do
      it 'serves the right bytes' do
        bytes = File.open("#{readme_resources}/cat.jpg", 'rb', &:read)
        expect(cat_image).to serve(eq(bytes))
      end
      it 'serves the right Content-Type' do
        expect(cat_image['Content-Type']).to eq('image/jpeg')
      end
    end
    context 'for an SVG' do
      it 'serves the right bytes' do
        bytes = File.open("#{readme_resources}/example.svg", 'rb', &:read)
        expect(svg_image).to serve(eq(bytes))
      end
      it 'serves the right Content-Type' do
        expect(svg_image['Content-Type']).to eq('image/svg+xml')
      end
    end
    it 'serves a very large image' do
      expect(very_large).to serve(eq(very_large_text))
    end
    context 'when you request a directory' do
      it 'redirects to index.html' do
        expect(directory).to redirect_to(eq('/guide/index.html'))
      end
    end
  end
  describe "when the host header doesn't have a `.` " \
           'it serves that master branch' do
    let(:host) { 'localhost' }
    let(:branch) { 'master' }
    # This doesn't support air gapped docs because we can't ask for them without
    # the `.`.
    include_examples 'serves some docs', supports_gapped: false
    include_context 'docs for branch'
    context 'the diff' do
      include_examples 'valid diff'
    end
  end
  describe 'for the test branch' do
    let(:branch) { 'test' }
    include_context 'docs for branch'
    include_examples '404s'
  end

  describe 'when we commit to the test branch of the target repo' do
    before(:context) do
      repo = @src.repo 'repo'
      repo.write 'index.asciidoc', <<~ASCIIDOC
        = Title

        [[moved_chapter]]
        == Chapter
        Some text.
      ASCIIDOC
      repo.commit 'test change for test branch'
      @dest.prepare_convert_all(@src.conf).target_branch('test').convert
    end
    it 'logs the fetch' do
      wait_for_logs(/\[new branch\]\s+test\s+->\s+test/)
      # The leading space in the second line is important because it causes
      # filebeat to group the two log lines.
      expect(logs).to include("\n" + <<~LOGS)
        From #{repo}
         * [new branch]      test       -> test
      LOGS
    end
    describe 'for the test branch' do
      let(:branch) { 'test' }
      include_context 'docs for branch'
      include_examples 'serves some docs'
      context 'the diff' do
        include_examples 'valid diff'
        it 'contains a link to the index which has changed' do
          expect(diff).to serve(include(<<~HTML))
            +4 -4 <a href="/guide/test/master/index.html">test/master/index.html</a>
          HTML
        end
        it 'contains a link to the moved chapter' do
          # TODO: We didn't *just* move the chapter. We dropped some images.
          # This seems like a mistake but fixing that breaks other tests.
          expect(diff).to serve(include(<<~HTML))
            +1 -16 <a href="/guide/test/master/moved_chapter.html">test/master/chapter.html -> test/master/moved_chapter.html</a>
          HTML
        end
        it "doesn't have a message saying there aren't any differences" do
          expect(diff).not_to serve(include(<<~HTML))
            <p>There aren't any differences!</p>
          HTML
        end
      end
      shared_examples 'logs the fetch' do
        it 'logs the fetch' do
          wait_for_logs(/#{@before_hash}\.\.#{@after_hash}\s+test\s+->\s+test/)
          # The leading space in the second line is important because it causes
          # filebeat to group the two log lines.
          expect(logs).to include("\n" + <<~LOGS)
            From #{repo}
               #{@before_hash}..#{@after_hash}  test       -> test
          LOGS
        end
      end
      describe 'when we modify the template' do
        before(:context) do
          # This simulates modifying the template in the docs repo and running
          # build_docs --all
          work = @src.repo 'work'
          work.clone_from @dest.bare_repo
          work.switch_to_branch 'test'
          @before_hash = work.short_hash
          old_template = work.read 'template.html'
          work.write 'template.html', old_template + 'trailing garbage'
          work.commit 'add garbage to template'
          work.push_to @dest.bare_repo
          @after_hash = work.short_hash
        end
        include_examples 'logs the fetch'
        it 'is immediately reflected in the root' do
          expect(guide_root).to serve(include(<<~HTML.strip))
            </html>\ntrailing garbage
          HTML
        end
      end
      describe 'for a very very large html page' do
        before(:context) do
          # This simulates adding a very large html page without having to
          # render it through asciidoc and docbook which would be very slow
          work = @src.repo 'work'
          @before_hash = work.short_hash
          work.write 'raw/very_large.html', <<~HTML
            <!DOCTYPE html>
            <html>
              <head><title>very large</title></head>
              <body>#{very_large_text}</body>
            </html>
          HTML
          work.commit 'add huge page'
          work.push_to @dest.bare_repo
          @after_hash = work.short_hash
        end
        let(:very_large_html) do
          get watermark, branch, 'guide/very_large.html'
        end
        include_examples 'logs the fetch'
        it 'serves the very large page without crashing' do
          expect(very_large_html).to serve(include(very_large_text))
        end
        it 'logs the access to the very large page' do
          wait_for_access '/guide/very_large.html'
          expect(logs).to include(<<~LOGS)
            #{watermark} #{host} GET /guide/very_large.html HTTP/1.1 200
          LOGS
        end
      end
      describe 'when we remove the template' do
        before(:context) do
          # This simulates what preview branches looked like before committing
          # the template.
          work = @src.repo 'work'
          @before_hash = work.short_hash
          work.delete 'template.html'
          work.commit 'remove template'
          work.push_to @dest.bare_repo
          @after_hash = work.short_hash
        end
        include_examples 'logs the fetch'
        describe 'everything still works because we fall back' do
          include_examples 'serves some docs', supports_gapped: false
        end
      end
    end
  end
  describe 'after we remove the test branch from the target repo' do
    before(:context) do
      @dest.remove_target_brach 'test'
    end
    it 'logs the fetch' do
      wait_for_logs(/\[deleted\]\s+\(none\)\s+->\s+test/)
      # The leading space in the second line is important because it causes
      # filebeat to group the two log lines.
      expect(logs).to include("\n" + <<~LOGS)
        From #{repo}
         - [deleted]         (none)     -> test
      LOGS
    end
    describe 'for the test branch' do
      let(:branch) { 'test' }
      include_context 'docs for branch'
      include_examples '404s'
    end
  end
  describe 'when we commit a noop change' do
    before(:context) do
      repo = @src.repo 'repo'
      repo.write 'index.asciidoc', <<~ASCIIDOC
        = Title

        [[chapter]]
        == Chapter
        Some text.

        image::resources/readme/cat.jpg[A cat]
        image::resources/readme/example.svg[An example svg]
        image::resources/very_large.jpg[Not a jpg but very big]
      ASCIIDOC
      repo.commit 'test change for test_noop branch2'
      @dest.prepare_convert_all(@src.conf).target_branch('test_noop').convert
    end
    it 'logs the fetch' do
      wait_for_logs(/\[new branch\]\s+test_noop\s+->\s+test_noop/)
      # The leading space in the second line is important because it causes
      # filebeat to group the two log lines.
      expect(logs).to include("\n" + <<~LOGS)
        From #{repo}
         * [new branch]      test_noop  -> test_noop
      LOGS
    end
    describe 'for the test branch' do
      let(:branch) { 'test_noop' }
      include_context 'docs for branch'
      include_examples 'serves some docs'
      context 'the diff' do
        include_examples 'valid diff'
        it 'is empty' do
          expect(diff).to serve(include("<ul>\n</ul>"))
        end
        it "has a message saying there aren't any differences" do
          expect(diff).to serve(include("<p>There aren't any differences!</p>"))
        end
      end
    end
  end
  describe 'when there are alternative examples' do
    before(:context) do
      # We don't have any examples in our source document so we'll just make a
      # dummy file so the checkout works. This is good enough to make a
      # distinct initial_js_state
      csharp_repo = @src.repo 'csharp'
      csharp_repo.write 'examples/dummy', 'dummy'
      csharp_repo.commit 'add example'

      book = @src.book 'Test'
      book.source(
        csharp_repo,
        'examples',
        alternatives: { source_lang: 'console', alternative_lang: 'csharp' }
      )
      @dest.prepare_convert_all(@src.conf)
           .target_branch('alternative_examples')
           .convert
    end
    it 'logs the fetch' do
      wait_for_logs(
        /\[new branch\]\s+alternative_examples\s+->\s+alternative_examples/
      )
      # The leading space in the second line is important because it causes
      # filebeat to group the two log lines.
      expect(logs).to include("\n" + <<~LOGS)
        From #{repo}
         * [new branch]      alternative_examples -> alternative_examples
      LOGS
    end
    let(:branch) { 'alternative_examples' }
    let(:expected_js_state) do
      {
        alternatives: {
          console: {
            csharp: { hasAny: false },
          },
        },
      }
    end
    include_context 'docs for branch'
    include_examples 'serves some docs'
  end
  describe 'when the language is something other than `en`' do
    before(:context) do
      book = @src.book 'Test'
      book.lang = 'foo'
      @dest.prepare_convert_all(@src.conf).target_branch('foolang').convert
    end
    it 'logs the fetch' do
      wait_for_logs(
        /\[new branch\]\s+foolang\s+->\s+foolang/
      )
      # The leading space in the second line is important because it causes
      # filebeat to group the two log lines.
      expect(logs).to include("\n" + <<~LOGS)
        From #{repo}
         * [new branch]      foolang    -> foolang
      LOGS
    end
    let(:branch) { 'foolang' }
    let(:expected_js_state) do
      {
        alternatives: {
          console: {
            csharp: { hasAny: false },
          },
        },
      }
    end
    let(:expected_language) { 'foo' }
    include_context 'docs for branch'
    include_examples 'serves some docs'
  end
  describe "when we can't pull from the repo" do
    before(:context) do
      FileUtils.rm_rf '/tmp/backup'
      FileUtils.mv @dest.bare_repo, '/tmp/backup'
    end
    it 'logs an angry message' do
      wait_for_logs(/Could not read from remote repository./)
    end
  end
  describe 'when we can pull again' do
    before(:context) do
      FileUtils.mv '/tmp/backup', @dest.bare_repo
      @dest.prepare_convert_all(@src.conf).target_branch('restored').convert
    end
    it 'fetches' do
      wait_for_logs(
        /\[new branch\]\s+restored\s+->\s+restored/
      )
      # The leading space in the second line is important because it causes
      # filebeat to group the two log lines.
      expect(logs).to include("\n" + <<~LOGS)
        From #{repo}
         * [new branch]      restored   -> restored
      LOGS
    end
    let(:branch) { 'restored' }
    include_context 'docs for branch'
    context 'the docs root' do
      it 'contains a link to the current index' do
        expect(guide_root).to serve(doc_body(include(<<~HTML.strip)))
          <a href="test/current/index.html" class="ulink" target="_top">Test</a>
        HTML
      end
      it 'logs access to the docs root' do
        wait_for_access '/'
        expect(logs).to include(<<~LOGS)
          #{watermark} #{host} GET /guide/index.html HTTP/1.1 200
        LOGS
      end
    end
  end
end
