# frozen_string_literal: true
require 'spec_helper'

# rubocop: disable RSpec/FactoriesInMigrationSpecs
RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover do
  let(:test_dir) { FileUploader.options['storage_path'] }
  let(:filename) { 'image.png' }

  let!(:namespace) { create(:namespace) }
  let!(:legacy_project) { create(:project, :legacy_storage, namespace: namespace) }
  let!(:hashed_project) { create(:project, namespace: namespace) }
  # default project
  let(:project) { legacy_project }

  let!(:issue) { create(:issue, project: project) }
  let!(:note) { create(:note, note: 'some note', project: project, noteable: issue) }

  let(:legacy_upload) { create_upload(note, filename) }

  def create_remote_upload(model, filename)
    create(:upload, :attachment_upload,
           path: "note/attachment/#{model.id}/#{filename}", secret: nil,
           store: ObjectStorage::Store::REMOTE, model: model)
  end

  def create_upload(model, filename, with_file = true)
    params = {
      path: "uploads/-/system/note/attachment/#{model.id}/#{filename}",
      model: model,
      store: ObjectStorage::Store::LOCAL
    }

    if with_file
      upload = create(:upload, :with_file, :attachment_upload, params)
      model.update(attachment: upload.retrieve_uploader)
      model.attachment.upload
    else
      create(:upload, :attachment_upload, params)
    end
  end

  def new_upload
    Upload.find_by(model_id: project.id, model_type: 'Project')
  end

  def expect_error_log
    expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger|
      expect(logger).to receive(:warn)
    end
  end

  shared_examples 'legacy upload deletion' do
    it 'removes the upload record' do
      described_class.new(legacy_upload).execute

      expect { legacy_upload.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end
  end

  shared_examples 'move error' do
    it 'does not remove the upload file' do
      expect_error_log

      described_class.new(legacy_upload).execute

      expect(legacy_upload.reload).to eq(legacy_upload)
    end
  end

  shared_examples 'migrates the file correctly' do
    before do
      described_class.new(legacy_upload).execute
    end

    it 'creates a new uplaod record correctly' do
      expect(new_upload.secret).not_to be_nil
      expect(new_upload.path).to end_with("#{new_upload.secret}/image.png")
      expect(new_upload.model_id).to eq(project.id)
      expect(new_upload.model_type).to eq('Project')
      expect(new_upload.uploader).to eq('FileUploader')
    end

    it 'updates the legacy upload note so that it references the file in the markdown' do
      expected_path = File.join('/uploads', new_upload.secret, 'image.png')
      expected_markdown = "some note \n ![image](#{expected_path})"
      expect(note.reload.note).to eq(expected_markdown)
    end

    it 'removes the attachment from the note model' do
      expect(note.reload.attachment.file).to be_nil
    end
  end

  context 'when no note found for the upload' do
    before do
      legacy_upload.model_id = nil
      legacy_upload.model_type = 'Note'
      expect_error_log
    end

    it_behaves_like 'legacy upload deletion'
  end

  context 'when upload does not belong to a note' do
    before do
      legacy_upload.model = create(:appearance)
    end

    it 'does not remove the upload' do
      expect { described_class.new(legacy_upload).execute }.not_to change { Upload.count }
    end
  end

  context 'when the upload move fails' do
    before do
      expect(FileUploader).to receive(:copy_to).and_raise('failed')
    end

    it_behaves_like 'move error'
  end

  context 'when the upload is in local storage' do
    shared_examples 'legacy local file' do
      it 'removes the file correctly' do
        expect(File.exist?(legacy_upload.absolute_path)).to be_truthy

        described_class.new(legacy_upload).execute

        expect(File.exist?(legacy_upload.absolute_path)).to be_falsey
      end

      it 'moves legacy uploads to the correct location' do
        described_class.new(legacy_upload).execute

        expected_path = File.join(test_dir, 'uploads', project.disk_path, new_upload.secret, filename)
        expect(File.exist?(expected_path)).to be_truthy
      end
    end

    context 'when the upload file does not exist on the filesystem' do
      let(:legacy_upload) { create_upload(note, filename, false) }

      before do
        expect_error_log
      end

      it_behaves_like 'legacy upload deletion'
    end

    context 'when an upload belongs to a legacy_diff_note' do
      let!(:merge_request) { create(:merge_request, source_project: project) }

      let!(:note) do
        create(:legacy_diff_note_on_merge_request,
          note: 'some note', project: project, noteable: merge_request)
      end

      let(:legacy_upload) do
        create(:upload, :with_file, :attachment_upload,
               path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note)
      end

      context 'when the file does not exist for the upload' do
        let(:legacy_upload) do
          create(:upload, :attachment_upload,
                 path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note)
        end

        it_behaves_like 'move error'
      end

      context 'when the file does not exist on expected path' do
        let(:legacy_upload) do
          create(:upload, :attachment_upload, :with_file,
                 path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}", model: note)
        end

        it_behaves_like 'move error'
      end

      context 'when the file path does not include system/note/attachment' do
        let(:legacy_upload) do
          create(:upload, :attachment_upload, :with_file,
                 path: "uploads/-/system#{note.id}/#{filename}", model: note)
        end

        it_behaves_like 'move error'
      end

      context 'when the file move raises an error' do
        before do
          allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
        end

        it_behaves_like 'move error'
      end

      context 'when upload has mount_point nil' do
        let(:legacy_upload) do
          create(:upload, :with_file, :attachment_upload,
                 path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note, mount_point: nil)
        end

        it_behaves_like 'migrates the file correctly'
        it_behaves_like 'legacy local file'
        it_behaves_like 'legacy upload deletion'
      end

      context 'when the file can be handled correctly' do
        it_behaves_like 'migrates the file correctly'
        it_behaves_like 'legacy local file'
        it_behaves_like 'legacy upload deletion'
      end
    end

    context 'when object storage is disabled for FileUploader' do
      context 'when the file belongs to a legacy project' do
        let(:project) { legacy_project }

        it_behaves_like 'migrates the file correctly'
        it_behaves_like 'legacy local file'
        it_behaves_like 'legacy upload deletion'
      end

      context 'when the file belongs to a hashed project' do
        let(:project) { hashed_project }

        it_behaves_like 'migrates the file correctly'
        it_behaves_like 'legacy local file'
        it_behaves_like 'legacy upload deletion'
      end
    end

    context 'when object storage is enabled for FileUploader' do
      # The process of migrating to object storage is a manual one,
      # so it would go against expectations to automatically migrate these files
      # to object storage during this migration.
      # After this migration, these files should be able to successfully migrate to object storage.

      before do
        stub_uploads_object_storage(FileUploader)
      end

      context 'when the file belongs to a legacy project' do
        let(:project) { legacy_project }

        it_behaves_like 'migrates the file correctly'
        it_behaves_like 'legacy local file'
        it_behaves_like 'legacy upload deletion'
      end

      context 'when the file belongs to a hashed project' do
        let(:project) { hashed_project }

        it_behaves_like 'migrates the file correctly'
        it_behaves_like 'legacy local file'
        it_behaves_like 'legacy upload deletion'
      end
    end
  end

  context 'when legacy uploads are stored in object storage' do
    let(:legacy_upload) { create_remote_upload(note, filename) }
    let(:remote_file) do
      { key: "#{legacy_upload.path}" }
    end

    let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
    let(:bucket) { connection.directories.create(key: 'uploads') }

    before do
      stub_uploads_object_storage(FileUploader)
    end

    shared_examples 'legacy remote file' do
      it 'removes the file correctly' do
        # expect(bucket.files.get(remote_file[:key])).to be_nil

        described_class.new(legacy_upload).execute

        expect(bucket.files.get(remote_file[:key])).to be_nil
      end

      it 'moves legacy uploads to the correct remote location' do
        described_class.new(legacy_upload).execute

        connection = ::Fog::Storage.new(FileUploader.object_store_credentials)
        expect(connection.get_object('uploads', new_upload.path)[:status]).to eq(200)
      end
    end

    context 'when the upload file does not exist on the filesystem' do
      it_behaves_like 'legacy upload deletion'
    end

    context 'when the file belongs to a legacy project' do
      before do
        bucket.files.create(remote_file)
      end

      let(:project) { legacy_project }

      it_behaves_like 'migrates the file correctly'
      it_behaves_like 'legacy remote file'
      it_behaves_like 'legacy upload deletion'
    end

    context 'when the file belongs to a hashed project' do
      before do
        bucket.files.create(remote_file)
      end

      let(:project) { hashed_project }

      it_behaves_like 'migrates the file correctly'
      it_behaves_like 'legacy remote file'
      it_behaves_like 'legacy upload deletion'
    end
  end
end
# rubocop: enable RSpec/FactoriesInMigrationSpecs
