# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Form::Import do
  subject { described_class.new(current_account: account, type: import_type, mode: import_mode, data: data) }

  let(:account)     { Fabricate(:account) }
  let(:data)        { fixture_file_upload(import_file) }
  let(:import_mode) { 'merge' }

  describe 'validations' do
    shared_examples 'incompatible import type' do |type, file|
      let(:import_file) { file }
      let(:import_type) { type }

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('imports.errors.incompatible_type'))
      end
    end

    shared_examples 'too many CSV rows' do |type, file, allowed_rows|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        stub_const 'Form::Import::ROWS_PROCESSING_LIMIT', allowed_rows
      end

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: described_class::ROWS_PROCESSING_LIMIT))
      end
    end

    shared_examples 'valid import' do |type, file|
      let(:import_file) { file }
      let(:import_type) { type }

      it 'passes validation' do
        expect(subject).to be_valid
      end
    end

    describe 'when the import type is custom_filters' do
      let(:data)        { fixture_file_upload(import_file, content_type) }
      let(:import_file) { File.open('spec/fixtures/files/custom_filters.json') }
      let(:import_type) { 'custom_filters' }
      let(:content_type) { 'application/json' }

      it 'passes validation' do
        expect(subject).to be_valid
      end
    end

    context 'when the file too large' do
      let(:import_type) { 'following' }
      let(:import_file) { 'imports.txt' }

      before do
        stub_const 'Form::Import::FILE_SIZE_LIMIT', 5
      end

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('imports.errors.too_large'))
      end
    end

    context 'when the CSV file is malformed CSV' do
      let(:import_type) { 'following' }
      let(:import_file) { 'boop.ogg' }

      it { is_expected.to_not allow_value(data).for(:data) }
    end

    context 'when importing more follows than allowed' do
      let(:import_type) { 'following' }
      let(:import_file) { 'imports.txt' }

      before do
        allow(FollowLimitValidator).to receive(:limit_for_account).with(account).and_return(1)
      end

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('users.follow_limit_reached', limit: 1))
      end
    end

    it_behaves_like 'too many CSV rows', 'following', 'imports.txt', 1
    it_behaves_like 'too many CSV rows', 'blocking', 'imports.txt', 1
    it_behaves_like 'too many CSV rows', 'muting', 'imports.txt', 1
    it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2
    it_behaves_like 'too many CSV rows', 'bookmarks', 'bookmark-imports.txt', 3
    it_behaves_like 'too many CSV rows', 'lists', 'lists.csv', 2

    # Importing list of addresses with no headers into various types
    it_behaves_like 'valid import', 'following', 'imports.txt'
    it_behaves_like 'valid import', 'blocking', 'imports.txt'
    it_behaves_like 'valid import', 'muting', 'imports.txt'

    # Importing domain blocks with headers into expected type
    it_behaves_like 'valid import', 'domain_blocking', 'domain_blocks.csv'

    # Importing bookmarks list with no headers into expected type
    it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt'

    # Importing lists with no headers into expected type
    it_behaves_like 'valid import', 'lists', 'lists.csv'

    # Importing followed accounts with headers into various compatible types
    it_behaves_like 'valid import', 'following', 'following_accounts.csv'
    it_behaves_like 'valid import', 'blocking', 'following_accounts.csv'
    it_behaves_like 'valid import', 'muting', 'following_accounts.csv'

    # Importing domain blocks with headers into incompatible types
    it_behaves_like 'incompatible import type', 'following', 'domain_blocks.csv'
    it_behaves_like 'incompatible import type', 'blocking', 'domain_blocks.csv'
    it_behaves_like 'incompatible import type', 'muting', 'domain_blocks.csv'
    it_behaves_like 'incompatible import type', 'bookmarks', 'domain_blocks.csv'

    # Importing followed accounts with headers into incompatible types
    it_behaves_like 'incompatible import type', 'domain_blocking', 'following_accounts.csv'
    it_behaves_like 'incompatible import type', 'bookmarks', 'following_accounts.csv'
  end

  describe '#guessed_type' do
    shared_examples 'with enough information' do |type, file, original_filename, expected_guess|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        allow(data).to receive(:original_filename).and_return(original_filename)
      end

      it 'guesses the expected type' do
        expect(subject.guessed_type).to eq expected_guess
      end
    end

    context 'when the headers are enough to disambiguate' do
      it_behaves_like 'with enough information', 'following', 'following_accounts.csv', 'import.csv', :following
      it_behaves_like 'with enough information', 'blocking', 'following_accounts.csv', 'import.csv', :following
      it_behaves_like 'with enough information', 'muting', 'following_accounts.csv', 'import.csv', :following

      it_behaves_like 'with enough information', 'following', 'muted_accounts.csv', 'imports.csv', :muting
      it_behaves_like 'with enough information', 'blocking', 'muted_accounts.csv', 'imports.csv', :muting
      it_behaves_like 'with enough information', 'muting', 'muted_accounts.csv', 'imports.csv', :muting
    end

    context 'when the file name is enough to disambiguate' do
      it_behaves_like 'with enough information', 'following', 'imports.txt', 'following_accounts.csv', :following
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'following_accounts.csv', :following
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'following_accounts.csv', :following

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'follows.csv', :following
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'follows.csv', :following
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'follows.csv', :following

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocked_accounts.csv', :blocking
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocked_accounts.csv', :blocking
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocked_accounts.csv', :blocking

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocks.csv', :blocking
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocks.csv', :blocking
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocks.csv', :blocking

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'muted_accounts.csv', :muting
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'muted_accounts.csv', :muting
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'muted_accounts.csv', :muting

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'mutes.csv', :muting
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'mutes.csv', :muting
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'mutes.csv', :muting
    end
  end

  describe '#likely_mismatched?' do
    shared_examples 'with matching types' do |type, file, original_filename = nil|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present?
      end

      it 'returns false' do
        expect(subject.likely_mismatched?).to be false
      end
    end

    shared_examples 'with mismatching types' do |type, file, original_filename = nil|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present?
      end

      it 'returns true' do
        expect(subject.likely_mismatched?).to be true
      end
    end

    it_behaves_like 'with matching types', 'following', 'following_accounts.csv'
    it_behaves_like 'with matching types', 'following', 'following_accounts.csv', 'imports.txt'
    it_behaves_like 'with matching types', 'following', 'imports.txt'
    it_behaves_like 'with matching types', 'blocking', 'imports.txt', 'blocks.csv'
    it_behaves_like 'with matching types', 'blocking', 'imports.txt'
    it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv'
    it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv', 'imports.txt'
    it_behaves_like 'with matching types', 'muting', 'imports.txt'
    it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv'
    it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv', 'imports.txt'
    it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt'
    it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt', 'imports.txt'

    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocks.csv'
    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocked_accounts.csv'
    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'mutes.csv'
    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'follows.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'mutes.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'follows.csv'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocks.csv'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocked_accounts.csv'
  end

  describe 'save' do
    shared_examples 'on successful import' do |type, mode, file, expected_rows|
      let(:import_type) { type }
      let(:import_file) { file }
      let(:import_mode) { mode }

      before { subject.save }

      context 'with a BulkImport' do
        let(:bulk_import) { account.bulk_imports.first }

        it 'creates a bulk import with correct values' do
          expect(bulk_import)
            .to be_present
            .and have_attributes(
              type: eq(subject.type),
              original_filename: eq(subject.data.original_filename),
              likely_mismatched?: eq(subject.likely_mismatched?),
              overwrite?: eq(!!subject.overwrite), # rubocop:disable Style/DoubleNegation
              processed_items: eq(0),
              imported_items: eq(0),
              total_items: eq(bulk_import.rows.count),
              state_unconfirmed?: be(true)
            )
          expect(bulk_import.rows.pluck(:data))
            .to match_array(expected_rows)
        end
      end
    end

    describe 'when importing json' do
      let(:import_type) { 'custom_filters' }
      let(:data) { fixture_file_upload('custom_filters.json', 'application/json') }
      let(:import_mode) { 'merge' }
      let(:expected_rows) do
        [
          {
            'title' => 'dfjswa',
            'expires_at' => nil,
            'context' => ['home'],
            'action' => 'warn',
            'keywords_attributes' => [{ 'keyword' => 'dvshja', 'whole_word' => true }],
            'statuses' => [],
          },
          {
            'title' => 'filter with a phrase as title',
            'expires_at' => nil,
            'context' => %w(home notifications public),
            'action' => 'warn',
            'keywords_attributes' => [
              { 'keyword' => "more words let's see ", 'whole_word' => true },
            ],
            'statuses' => [],
          },
          {
            'title' => 'how do I add a status to a filter?',
            'expires_at' => nil,
            'context' => ['public', 'account'],
            'action' => 'warn',
            'keywords_attributes' => [
              { 'keyword' => 'something something', 'whole_word' => true },
            ],
            'statuses' => [],
          },
        ]
      end

      before do
        subject.save
      end

      context 'with a BulkImport' do
        let(:bulk_import) { account.bulk_imports.first }

        it 'creates a bulk import with correct values' do
          expect(bulk_import)
            .to be_present
            .and have_attributes(
              type: eq(subject.type),
              original_filename: eq(subject.data.original_filename),
              likely_mismatched?: eq(subject.likely_mismatched_json?),
              overwrite?: eq(!!subject.overwrite), # rubocop:disable Style/DoubleNegation
              processed_items: eq(0),
              imported_items: eq(0),
              total_items: eq(bulk_import.rows.count),
              state_unconfirmed?: be(true)
            )
          expect(bulk_import.rows.pluck(:data))
            .to match_array(expected_rows)
        end
      end
    end

    it_behaves_like('on successful import', 'following', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like('on successful import', 'following', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like('on successful import', 'blocking', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like('on successful import', 'blocking', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like('on successful import', 'muting', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like('on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', %w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } })
    it_behaves_like('on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', %w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } })

    it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [
      { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil },
      { 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => %w(en fr) },
    ]

    it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [
      { 'acct' => 'user@example.com', 'hide_notifications' => true },
      { 'acct' => 'user@test.com', 'hide_notifications' => false },
    ]

    it_behaves_like 'on successful import', 'lists', 'merge', 'lists.csv', [
      { 'acct' => 'gargron@example.com', 'list_name' => 'Mastodon project' },
      { 'acct' => 'mastodon@example.com', 'list_name' => 'Mastodon project' },
      { 'acct' => 'foo@example.com', 'list_name' => 'test' },
    ]

    # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
    #
    # https://github.com/mastodon/mastodon/issues/20571
    it_behaves_like 'on successful import', 'following', 'merge', 'utf8-followers.txt', [{ 'acct' => 'nare@թութ.հայ' }]
  end
end
