require 'rails_helper'

RSpec.describe 'Tags deerjikists API', type: :request do
  let(:platform1) { 'nico' }
  let(:code1) { 'deerjika-bot' }
  let(:platform2) { 'youtube' }
  let(:code2) { 'deerjika-bot.bsky.social' }

  let!(:tag) { create(:tag, category: :deerjikist) }

  let(:member) { create(:user, :member) }
  let(:guest) { create(:user, role: :guest) }

  before do
    tag.tag_name.update!(name: 'deerjika')
  end

  describe 'GET /tags/:id/deerjikists' do
    subject(:do_request) do
      get "/tags/#{tag_id}/deerjikists"
    end

    let(:tag_id) { tag.id }

    context 'when tag exists and has no deerjikists' do
      it 'returns 200 with tag and empty deerjikists array' do
        do_request

        expect(response).to have_http_status(:ok)

        expect(json).to be_a(Hash)

        expect(json['tag']).to be_a(Hash)
        expect(json['tag']['id']).to eq(tag.id)
        expect(json['tag']['name']).to eq('deerjika')
        expect(json['tag']['category']).to eq('deerjikist')
        expect(json['tag']['has_deerjikists']).to eq(false)

        expect(json['deerjikists']).to eq([])
      end
    end

    context 'when tag exists and has deerjikists' do
      before do
        Deerjikist.create!(platform: platform1, code: code1, tag: tag)
        Deerjikist.create!(platform: platform2, code: code2, tag: tag)
      end

      it 'returns 200 with tag and deerjikists array' do
        do_request

        expect(response).to have_http_status(:ok)

        expect(json).to be_a(Hash)

        expect(json['tag']).to be_a(Hash)
        expect(json['tag']['id']).to eq(tag.id)
        expect(json['tag']['name']).to eq('deerjika')
        expect(json['tag']['category']).to eq('deerjikist')
        expect(json['tag']['has_deerjikists']).to eq(true)

        expect(json['deerjikists']).to be_a(Array)
        expect(json['deerjikists'].size).to eq(2)

        expect(json['deerjikists'].map { |h| [h['platform'], h['code']] })
          .to contain_exactly(
            [platform1, code1],
            [platform2, code2],
          )
      end
    end

    context 'when tag does not exist' do
      let(:tag_id) { 9_999_999 }

      it 'returns 404' do
        do_request

        expect(response).to have_http_status(:not_found)
      end
    end
  end

  describe 'GET /tags/name/:name/deerjikists' do
    subject(:do_request) do
      get "/tags/name/#{name}/deerjikists"
    end

    let(:name) { 'deerjika' }

    context 'when name is blank after strip' do
      let(:name) { '%20' }

      it 'returns 400' do
        do_request

        expect(response).to have_http_status(:bad_request)
      end
    end

    context 'when tag does not exist for name' do
      let(:name) { 'no-such-tag' }

      it 'returns 404' do
        do_request

        expect(response).to have_http_status(:not_found)
      end
    end

    context 'when tag exists and has no deerjikists' do
      it 'returns 200 with tag and empty deerjikists array' do
        do_request

        expect(response).to have_http_status(:ok)

        expect(json).to be_a(Hash)

        expect(json['tag']).to be_a(Hash)
        expect(json['tag']['id']).to eq(tag.id)
        expect(json['tag']['name']).to eq('deerjika')
        expect(json['tag']['category']).to eq('deerjikist')
        expect(json['tag']['has_deerjikists']).to eq(false)

        expect(json['deerjikists']).to eq([])
      end
    end

    context 'when tag exists and has deerjikists' do
      before do
        Deerjikist.create!(platform: platform1, code: code1, tag: tag)
      end

      it 'returns 200 with tag and deerjikists array' do
        do_request

        expect(response).to have_http_status(:ok)

        expect(json).to be_a(Hash)

        expect(json['tag']).to be_a(Hash)
        expect(json['tag']['id']).to eq(tag.id)
        expect(json['tag']['name']).to eq('deerjika')
        expect(json['tag']['category']).to eq('deerjikist')
        expect(json['tag']['has_deerjikists']).to eq(true)

        expect(json['deerjikists']).to be_a(Array)
        expect(json['deerjikists'].size).to eq(1)

        expect(json['deerjikists'][0]['platform']).to eq(platform1)
        expect(json['deerjikists'][0]['code']).to eq(code1)
      end
    end
  end

  describe 'PUT /tags/:id/deerjikists' do
    subject(:do_request) do
      put "/tags/#{tag_id}/deerjikists", params: payload, as: :json
    end

    let(:tag_id) { tag.id }
    let(:payload) do
      [
        { platform: platform1, code: code1 },
        { platform: platform2, code: code2 },
      ]
    end

    context 'when not logged in' do
      it 'returns 401' do
        do_request

        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'when logged in but not member' do
      before do
        sign_in_as guest
      end

      it 'returns 403' do
        do_request

        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'when tag does not exist' do
      let(:tag_id) { 9_999_999 }

      before do
        sign_in_as member
      end

      it 'returns 404' do
        do_request

        expect(response).to have_http_status(:not_found)
      end
    end

    context 'when logged in as member' do
      before do
        sign_in_as member
      end

      context 'when tag has no deerjikists' do
        it 'creates deerjikists and returns deerjikists array' do
          expect {
            do_request
          }.to change { Deerjikist.where(tag: tag).count }.from(0).to(2)

          expect(response).to have_http_status(:ok)

          expect(json).to be_a(Array)
          expect(json.map { |h| [h['platform'], h['code']] })
            .to contain_exactly(
              [platform1, code1],
              [platform2, code2],
            )

          expect(Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] })
            .to contain_exactly(
              [platform1, code1],
              [platform2, code2],
            )
        end
      end

      context 'when tag already has deerjikists' do
        before do
          Deerjikist.create!(platform: platform1, code: 'old-code-1', tag: tag)
          Deerjikist.create!(platform: platform2, code: 'old-code-2', tag: tag)
        end

        it 'replaces deerjikists and returns deerjikists array' do
          do_request

          expect(response).to have_http_status(:ok)

          expect(Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] })
            .to contain_exactly(
              [platform1, code1],
              [platform2, code2],
            )

          expect(Deerjikist.exists?(platform: platform1, code: 'old-code-1')).to eq(false)
          expect(Deerjikist.exists?(platform: platform2, code: 'old-code-2')).to eq(false)

          expect(json).to be_a(Array)
          expect(json.map { |h| [h['platform'], h['code']] })
            .to contain_exactly(
              [platform1, code1],
              [platform2, code2],
            )
        end
      end

      context 'when payload is empty array' do
        let(:payload) { [] }

        before do
          Deerjikist.create!(platform: platform1, code: code1, tag: tag)
          Deerjikist.create!(platform: platform2, code: code2, tag: tag)
        end

        it 'clears deerjikists and returns empty array' do
          expect {
            do_request
          }.to change { Deerjikist.where(tag: tag).count }.from(2).to(0)

          expect(response).to have_http_status(:ok)
          expect(json).to eq([])
        end
      end

      context 'when a row is invalid' do
        let(:payload) do
          [
            { platform: '', code: code1 },
          ]
        end

        it 'returns 422 with indexed field errors and does not replace existing deerjikists' do
          Deerjikist.create!(platform: platform1, code: code1, tag: tag)

          expect {
            do_request
          }.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }

          expect(response).to have_http_status(:unprocessable_entity)
          expect(json).to include(
              'type' => 'validation_error',
              'message' => '入力内容を確認してください．',
              'base_errors' => [])
          expect(json.fetch('errors')).to include(
              'deerjikists.0.platform' => [be_present])
        end
      end

      context 'when youtube code is handle' do
        let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
        let(:payload) do
          [
            { platform: 'youtube', code: '@deerjika' },
          ]
        end

        before do
          allow(Net::HTTP).to receive(:get).and_return(
            %(<link rel="canonical" href="https://www.youtube.com/channel/#{channel_id}">),
          )
        end

        it 'normalises youtube handle to channel id' do
          expect {
            do_request
          }.to change { Deerjikist.where(tag: tag).count }.from(0).to(1)

          expect(response).to have_http_status(:ok)

          expect(Net::HTTP).to have_received(:get)

          expect(Deerjikist.exists?(platform: 'youtube', code: channel_id, tag: tag))
            .to eq(true)

          expect(json).to be_a(Array)
          expect(json.size).to eq(1)
          expect(json[0]['platform']).to eq('youtube')
          expect(json[0]['code']).to eq(channel_id)
        end
      end
    end
  end
end
