# frozen_string_literal: true

require "spec_helper"

describe WikiPage do
  let(:project) { create(:project, :wiki_repo) }
  let(:user) { project.owner }
  let(:wiki) { ProjectWiki.new(project, user) }

  let(:new_page) do
    described_class.new(wiki).tap do |page|
      page.attributes = { title: 'test page', content: 'test content' }
    end
  end

  let(:existing_page) do
    create_page('test page', 'test content')
    wiki.find_page('test page')
  end

  subject { new_page }

  describe '.group_by_directory' do
    context 'when there are no pages' do
      it 'returns an empty array' do
        expect(described_class.group_by_directory(nil)).to eq([])
        expect(described_class.group_by_directory([])).to eq([])
      end
    end

    context 'when there are pages' do
      before do
        create_page('dir_1/dir_1_1/page_3', 'content')
        create_page('page_1', 'content')
        create_page('dir_1/page_2', 'content')
        create_page('dir_2', 'page with dir name')
        create_page('dir_2/page_5', 'content')
        create_page('page_6', 'content')
        create_page('dir_2/page_4', 'content')
      end

      let(:page_1) { wiki.find_page('page_1') }
      let(:page_6) { wiki.find_page('page_6') }
      let(:page_dir_2) { wiki.find_page('dir_2') }

      let(:dir_1) do
        WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
      end
      let(:dir_1_1) do
        WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')])
      end
      let(:dir_2) do
        pages = [wiki.find_page('dir_2/page_5'),
                 wiki.find_page('dir_2/page_4')]
        WikiDirectory.new('dir_2', pages)
      end

      describe "#list_pages" do
        context 'sort by title' do
          let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) }
          let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] }

          it 'returns an array with pages and directories' do
            grouped_entries.each_with_index do |page_or_dir, i|
              expected_page_or_dir = expected_grouped_entries[i]
              expected_slugs = get_slugs(expected_page_or_dir)
              slugs = get_slugs(page_or_dir)

              expect(slugs).to match_array(expected_slugs)
            end
          end
        end

        context 'sort by created_at' do
          let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) }
          let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] }

          it 'returns an array with pages and directories' do
            grouped_entries.each_with_index do |page_or_dir, i|
              expected_page_or_dir = expected_grouped_entries[i]
              expected_slugs = get_slugs(expected_page_or_dir)
              slugs = get_slugs(page_or_dir)

              expect(slugs).to match_array(expected_slugs)
            end
          end
        end

        it 'returns an array with retained order with directories at the top' do
          expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']

          grouped_entries = described_class.group_by_directory(wiki.list_pages)

          actual_order =
            grouped_entries.flat_map do |page_or_dir|
              get_slugs(page_or_dir)
            end
          expect(actual_order).to eq(expected_order)
        end
      end
    end
  end

  describe '.unhyphenize' do
    it 'removes hyphens from a name' do
      name = 'a-name--with-hyphens'

      expect(described_class.unhyphenize(name)).to eq('a name with hyphens')
    end
  end

  describe "#initialize" do
    context "when initialized with an existing page" do
      subject { existing_page }

      it "sets the slug attribute" do
        expect(subject.slug).to eq("test-page")
      end

      it "sets the title attribute" do
        expect(subject.title).to eq("test page")
      end

      it "sets the formatted content attribute" do
        expect(subject.content).to eq("test content")
      end

      it "sets the format attribute" do
        expect(subject.format).to eq(:markdown)
      end

      it "sets the message attribute" do
        expect(subject.message).to eq("test commit")
      end

      it "sets the version attribute" do
        expect(subject.version).to be_a Gitlab::Git::WikiPageVersion
      end
    end
  end

  describe "validations" do
    it "validates presence of title" do
      subject.attributes.delete(:title)

      expect(subject).not_to be_valid
      expect(subject.errors.keys).to contain_exactly(:title)
    end

    it "validates presence of content" do
      subject.attributes.delete(:content)

      expect(subject).not_to be_valid
      expect(subject.errors.keys).to contain_exactly(:content)
    end

    describe '#validate_path_limits' do
      let(:max_title) { described_class::MAX_TITLE_BYTES }
      let(:max_directory) { described_class::MAX_DIRECTORY_BYTES }

      where(:character) do
        ['a', 'รค', '๐Ÿ™ˆ']
      end

      with_them do
        let(:size) { character.bytesize.to_f }
        let(:valid_title) { character * (max_title / size).floor }
        let(:valid_directory) { character * (max_directory / size).floor }
        let(:invalid_title) { character * ((max_title + 1) / size).ceil }
        let(:invalid_directory) { character * ((max_directory + 1) / size).ceil }

        it 'accepts page titles below the limit' do
          subject.title = valid_title

          expect(subject).to be_valid
        end

        it 'accepts directories below the limit' do
          subject.title = valid_directory + '/foo'

          expect(subject).to be_valid
        end

        it 'accepts a path with page title and directory below the limit' do
          subject.title = "#{valid_directory}/#{valid_title}"

          expect(subject).to be_valid
        end

        it 'rejects page titles exceeding the limit' do
          subject.title = invalid_title

          expect(subject).not_to be_valid
          expect(subject.errors[:title]).to contain_exactly(
            "exceeds the limit of #{max_title} bytes"
          )
        end

        it 'rejects directories exceeding the limit' do
          subject.title = "#{invalid_directory}/#{invalid_directory}2/foo"

          expect(subject).not_to be_valid
          expect(subject.errors[:title]).to contain_exactly(
            "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}\"",
            "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}2\""
          )
        end

        it 'rejects a page with both title and directory exceeding the limit' do
          subject.title = "#{invalid_directory}/#{invalid_title}"

          expect(subject).not_to be_valid
          expect(subject.errors[:title]).to contain_exactly(
            "exceeds the limit of #{max_title} bytes",
            "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}\""
          )
        end
      end

      context 'with an existing page title exceeding the limit' do
        subject do
          title = 'a' * (max_title + 1)
          create_page(title, 'content')
          wiki.find_page(title)
        end

        it 'accepts the exceeding title length when unchanged' do
          expect(subject).to be_valid
        end

        it 'rejects the exceeding title length when changed' do
          subject.title = 'b' * (max_title + 1)

          expect(subject).not_to be_valid
          expect(subject.errors).to include(:title)
        end
      end
    end
  end

  describe "#create" do
    let(:attributes) do
      {
        title: "Index",
        content: "Home Page",
        format: "markdown",
        message: 'Custom Commit Message'
      }
    end

    context "with valid attributes" do
      it "saves the wiki page" do
        subject.create(attributes)

        expect(wiki.find_page("Index")).not_to be_nil
      end

      it "returns true" do
        expect(subject.create(attributes)).to eq(true)
      end

      it 'saves the wiki page with message' do
        subject.create(attributes)

        expect(wiki.find_page("Index").message).to eq 'Custom Commit Message'
      end
    end
  end

  describe "dot in the title" do
    let(:title) { 'Index v1.2.3' }

    describe "#create" do
      let(:attributes) { { title: title, content: "Home Page", format: "markdown" } }

      context "with valid attributes" do
        it "saves the wiki page" do
          subject.create(attributes)

          expect(wiki.find_page(title)).not_to be_nil
        end

        it "returns true" do
          expect(subject.create(attributes)).to eq(true)
        end
      end
    end

    describe "#update" do
      subject do
        create_page(title, "content")
        wiki.find_page(title)
      end

      it "updates the content of the page" do
        subject.update(content: "new content")
        page = wiki.find_page(title)

        expect(page.content).to eq('new content')
      end

      it "returns true" do
        expect(subject.update(content: "more content")).to be_truthy
      end
    end
  end

  describe '#create' do
    context 'with valid attributes' do
      it 'raises an error if a page with the same path already exists' do
        create_page('New Page', 'content')
        create_page('foo/bar', 'content')

        expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
        expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
      end

      it 'if the title is preceded by a / it is removed' do
        create_page('/New Page', 'content')

        expect(wiki.find_page('New Page')).not_to be_nil
      end
    end
  end

  describe "#update" do
    subject { existing_page }

    context "with valid attributes" do
      it "updates the content of the page" do
        new_content = "new content"

        subject.update(content: new_content)
        page = wiki.find_page('test page')

        expect(page.content).to eq("new content")
      end

      it "updates the title of the page" do
        new_title = "Index v.1.2.4"

        subject.update(title: new_title)
        page = wiki.find_page(new_title)

        expect(page.title).to eq(new_title)
      end

      it "returns true" do
        expect(subject.update(content: "more content")).to be_truthy
      end
    end

    context 'with same last commit sha' do
      it 'returns true' do
        expect(subject.update(content: 'more content', last_commit_sha: subject.last_commit_sha)).to be_truthy
      end
    end

    context 'with different last commit sha' do
      it 'raises exception' do
        expect { subject.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
      end
    end

    context 'when renaming a page' do
      it 'raises an error if the page already exists' do
        create_page('Existing Page', 'content')

        expect { subject.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
        expect(subject.title).to eq 'test page'
        expect(subject.content).to eq 'new_content'
      end

      it 'updates the content and rename the file' do
        new_title = 'Renamed Page'
        new_content = 'updated content'

        expect(subject.update(title: new_title, content: new_content)).to be_truthy

        page = wiki.find_page(new_title)

        expect(page).not_to be_nil
        expect(page.content).to eq new_content
      end
    end

    context 'when moving a page' do
      it 'raises an error if the page already exists' do
        create_page('foo/Existing Page', 'content')

        expect { subject.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
        expect(subject.title).to eq 'test page'
        expect(subject.content).to eq 'new_content'
      end

      it 'updates the content and moves the file' do
        new_title = 'foo/Other Page'
        new_content = 'new_content'

        expect(subject.update(title: new_title, content: new_content)).to be_truthy

        page = wiki.find_page(new_title)

        expect(page).not_to be_nil
        expect(page.content).to eq new_content
      end

      context 'in subdir' do
        subject do
          create_page('foo/Existing Page', 'content')
          wiki.find_page('foo/Existing Page')
        end

        it 'moves the page to the root folder if the title is preceded by /' do
          expect(subject.slug).to eq 'foo/Existing-Page'
          expect(subject.update(title: '/Existing Page', content: 'new_content')).to be_truthy
          expect(subject.slug).to eq 'Existing-Page'
        end

        it 'does nothing if it has the same title' do
          original_path = subject.slug

          expect(subject.update(title: 'Existing Page', content: 'new_content')).to be_truthy
          expect(subject.slug).to eq original_path
        end
      end

      context 'in root dir' do
        it 'does nothing if the title is preceded by /' do
          original_path = subject.slug

          expect(subject.update(title: '/test page', content: 'new_content')).to be_truthy
          expect(subject.slug).to eq original_path
        end
      end
    end

    context "with invalid attributes" do
      it 'aborts update if title blank' do
        expect(subject.update(title: '', content: 'new_content')).to be_falsey
        expect(subject.content).to eq 'new_content'

        page = wiki.find_page('test page')

        expect(page.content).to eq 'test content'
      end
    end
  end

  describe "#destroy" do
    subject { existing_page }

    it "deletes the page" do
      subject.delete

      expect(wiki.list_pages).to be_empty
    end

    it "returns true" do
      expect(subject.delete).to eq(true)
    end
  end

  describe "#versions" do
    subject { existing_page }

    it "returns an array of all commits for the page" do
      3.times { |i| subject.update(content: "content #{i}") }

      expect(subject.versions.count).to eq(4)
    end

    it 'returns instances of WikiPageVersion' do
      expect(subject.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
    end
  end

  describe '#title_changed?' do
    using RSpec::Parameterized::TableSyntax

    let(:untitled_page) { described_class.new(wiki) }
    let(:directory_page) do
      create_page('parent/child', 'test content')
      wiki.find_page('parent/child')
    end

    where(:page, :title, :changed) do
      :untitled_page  | nil            | false
      :untitled_page  | 'new title'    | true

      :new_page       | nil            | true
      :new_page       | 'test page'    | true
      :new_page       | 'new title'    | true

      :existing_page  | nil            | false
      :existing_page  | 'test page'    | false
      :existing_page  | '/test page'   | false
      :existing_page  | 'new title'    | true

      :directory_page | nil            | false
      :directory_page | 'parent/child' | false
      :directory_page | 'child'        | false
      :directory_page | '/child'       | true
      :directory_page | 'parent/other' | true
      :directory_page | 'other/child'  | true
    end

    with_them do
      it 'returns the expected value' do
        subject = public_send(page)
        subject.title = title if title

        expect(subject.title_changed?).to be(changed)
      end
    end
  end

  describe '#path' do
    it 'returns the path when persisted' do
      expect(existing_page.path).to eq('test-page.md')
    end

    it 'returns nil when not persisted' do
      expect(new_page.path).to be_nil
    end
  end

  describe '#directory' do
    context 'when the page is at the root directory' do
      subject { existing_page }

      it 'returns an empty string' do
        expect(subject.directory).to eq('')
      end
    end

    context 'when the page is inside an actual directory' do
      subject do
        create_page('dir_1/dir_1_1/file', 'content')
        wiki.find_page('dir_1/dir_1_1/file')
      end

      it 'returns the full directory hierarchy' do
        expect(subject.directory).to eq('dir_1/dir_1_1')
      end
    end
  end

  describe '#historical?' do
    subject { existing_page }

    let(:old_version) { subject.versions.last.id }
    let(:old_page) { wiki.find_page(subject.title, old_version) }
    let(:latest_version) { subject.versions.first.id }
    let(:latest_page) { wiki.find_page(subject.title, latest_version) }

    before do
      3.times { |i| subject.update(content: "content #{i}") }
    end

    it 'returns true when requesting an old version' do
      expect(old_page.historical?).to be_truthy
    end

    it 'returns false when requesting latest version' do
      expect(latest_page.historical?).to be_falsy
    end

    it 'returns false when version is nil' do
      expect(latest_page.historical?).to be_falsy
    end

    it 'returns false when the last version is nil' do
      expect(old_page).to receive(:last_version) { nil }

      expect(old_page.historical?).to be_falsy
    end

    it 'returns false when the version is nil' do
      expect(old_page).to receive(:version) { nil }

      expect(old_page.historical?).to be_falsy
    end
  end

  describe '#to_partial_path' do
    it 'returns the relative path to the partial to be used' do
      expect(subject.to_partial_path).to eq('projects/wikis/wiki_page')
    end
  end

  describe '#==' do
    subject { existing_page }

    it 'returns true for identical wiki page' do
      expect(subject).to eq(subject)
    end

    it 'returns false for updated wiki page' do
      subject.update(content: "Updated content")
      updated_page = wiki.find_page('test page')

      expect(updated_page).not_to be_nil
      expect(updated_page).not_to eq(subject)
    end
  end

  describe '#last_commit_sha' do
    subject { existing_page }

    it 'returns commit sha' do
      expect(subject.last_commit_sha).to eq subject.last_version.sha
    end

    it 'is changed after page updated' do
      last_commit_sha_before_update = subject.last_commit_sha

      subject.update(content: "new content")
      page = wiki.find_page('test page')

      expect(page.last_commit_sha).not_to eq last_commit_sha_before_update
    end
  end

  describe '#hook_attrs' do
    it 'adds absolute urls for images in the content' do
      subject.attributes[:content] = 'test![WikiPage_Image](/uploads/abc/WikiPage_Image.png)'

      expect(subject.hook_attrs['content']).to eq("test![WikiPage_Image](#{Settings.gitlab.url}/uploads/abc/WikiPage_Image.png)")
    end
  end

  private

  def remove_temp_repo(path)
    FileUtils.rm_rf path
  end

  def commit_details
    Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
  end

  def create_page(name, content)
    wiki.wiki.write_page(name, :markdown, content, commit_details)
  end

  def get_slugs(page_or_dir)
    if page_or_dir.is_a? WikiPage
      [page_or_dir.slug]
    else
      page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : []
    end
  end
end