require 'rails_helper'
require 'active_support/testing/time_helpers'


RSpec.describe 'Theatres API', type: :request do
  include ActiveSupport::Testing::TimeHelpers

  around do |example|
    travel_to(Time.zone.parse('2026-03-18 21:00:00')) do
      example.run
    end
  end

  let(:member) { create(:user, :member, name: 'member user') }
  let(:other_user) { create(:user, :member, name: 'other user') }

  let!(:niconico_post) do
    Post.create!(
      title: 'niconico post',
      url: 'https://www.nicovideo.jp/watch/sm123'
    )
  end

  let!(:second_niconico_post) do
    Post.create!(
      title: 'second niconico post',
      url: 'https://www.nicovideo.jp/watch/sm456'
    )
  end

  let!(:youtube_post) do
    Post.create!(
      title: 'youtube post',
      url: 'https://www.youtube.com/watch?v=yt123'
    )
  end

  let!(:other_post) do
    Post.create!(
      title: 'other post',
      url: 'https://example.com/posts/1'
    )
  end

  let!(:theatre) do
    Theatre.create!(
      name: 'spec theatre',
      opens_at: Time.zone.parse('2026-03-18 20:00:00'),
      kind: 0,
      created_by_user: member
    )
  end

  describe 'GET /theatres/:id' do
    subject(:do_request) do
      get "/theatres/#{theatre_id}"
    end

    context 'when theatre exists' do
      let(:theatre_id) { theatre.id }

      it 'returns theatre json' do
        do_request

        expect(response).to have_http_status(:ok)
        expect(json).to include(
          'id' => theatre.id,
          'name' => 'spec theatre'
        )
        expect(json).to have_key('opens_at')
        expect(json).to have_key('closes_at')
        expect(json).to have_key('created_at')
        expect(json).to have_key('updated_at')

        expect(json['created_by_user']).to include(
          'id' => member.id,
          'name' => 'member user'
        )
      end
    end

    context 'when theatre does not exist' do
      let(:theatre_id) { 999_999_999 }

      it 'returns 404' do
        do_request
        expect(response).to have_http_status(:not_found)
      end
    end
  end

  describe 'PUT /theatres/:id/watching' do
    subject(:do_request) do
      put "/theatres/#{theatre_id}/watching"
    end

    let(:theatre_id) { theatre.id }

    context 'when not logged in' do
      it 'returns 401' do
        sign_out
        do_request
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'when theatre does not exist' do
      let(:theatre_id) { 999_999_999 }

      it 'returns 404' do
        sign_in_as(member)
        do_request
        expect(response).to have_http_status(:not_found)
      end
    end

    context 'when theatre has no host yet' do
      before do
        sign_in_as(member)
      end

      it 'creates watching row, assigns current user as host, and returns current theatre info' do
        expect { do_request }
          .to change { TheatreWatchingUser.count }.by(1)

        expect(response).to have_http_status(:ok)

        theatre.reload
        watch = TheatreWatchingUser.find_by!(theatre: theatre, user: member)

        expect(theatre.host_user_id).to eq(member.id)
        expect(watch.expires_at).to be_within(1.second).of(30.seconds.from_now)

        expect(json).to include(
          'host_flg' => true,
          'post_id' => nil,
          'post_started_at' => nil,
          'post_elapsed_ms' => nil
        )

        expect(json.fetch('watching_users')).to contain_exactly(
          {
            'id' => member.id,
            'name' => 'member user'
          }
        )
      end
    end

    context 'when current user is already watching' do
      let!(:watching_row) do
        TheatreWatchingUser.create!(
          theatre: theatre,
          user: member,
          expires_at: 5.seconds.from_now
        )
      end

      before do
        sign_in_as(member)
      end

      it 'refreshes expires_at without creating another row' do
        expect { do_request }
          .not_to change { TheatreWatchingUser.count }

        expect(response).to have_http_status(:ok)

        expect(watching_row.reload.expires_at)
          .to be_within(1.second).of(30.seconds.from_now)
      end
    end

    context 'when another active host exists' do
      before do
        TheatreWatchingUser.create!(
          theatre: theatre,
          user: other_user,
          expires_at: 10.minutes.from_now
        )
        theatre.update!(host_user: other_user)
        sign_in_as(member)
      end

      it 'does not steal host and returns host_flg false' do
        expect { do_request }
          .to change { TheatreWatchingUser.count }.by(1)

        expect(response).to have_http_status(:ok)
        expect(theatre.reload.host_user_id).to eq(other_user.id)

        expect(json).to include(
          'host_flg' => false,
          'post_id' => nil,
          'post_started_at' => nil,
          'post_elapsed_ms' => nil
        )

        expect(json.fetch('watching_users')).to contain_exactly(
          {
            'id' => member.id,
            'name' => 'member user'
          },
          {
            'id' => other_user.id,
            'name' => 'other user'
          }
        )
      end
    end

    context 'when host is set but no longer actively watching' do
      let(:started_at) { 2.minutes.ago }

      before do
        TheatreWatchingUser.create!(
          theatre: theatre,
          user: other_user,
          expires_at: 1.second.ago
        )
        theatre.update!(
          host_user: other_user,
          current_post: niconico_post,
          current_post_started_at: started_at
        )
        sign_in_as(member)
      end

      it 'reassigns host to current user and returns current post info' do
        expect { do_request }
          .to change { TheatreWatchingUser.count }.by(1)

        expect(response).to have_http_status(:ok)

        theatre.reload
        expect(theatre.host_user_id).to eq(member.id)

        expect(json['host_flg']).to eq(true)
        expect(json['post_id']).to eq(niconico_post.id)
        expect(Time.zone.parse(json['post_started_at']))
          .to be_within(1.second).of(started_at)
        expect(json['post_elapsed_ms'])
          .to be_within(1_000).of(120_000)
      end
    end
  end

  describe 'PATCH /theatres/:id/next_post' do
    subject(:do_request) do
      patch "/theatres/#{theatre_id}/next_post"
    end

    let(:theatre_id) { theatre.id }

    context 'when not logged in' do
      it 'returns 401' do
        sign_out
        do_request
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'when theatre does not exist' do
      let(:theatre_id) { 999_999_999 }

      it 'returns 404' do
        sign_in_as(member)
        do_request
        expect(response).to have_http_status(:not_found)
      end
    end

    context 'when logged in but not host' do
      before do
        theatre.update!(host_user: other_user)
        sign_in_as(member)
      end

      it 'returns 403' do
        do_request
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'when current user is host' do
      before do
        theatre.update!(host_user: member)
        sign_in_as(member)
      end

      it 'sets current_post to an eligible post and updates current_post_started_at' do
        expect { do_request }
          .to change { theatre.reload.current_post_id }

        expect(response).to have_http_status(:no_content)
        expect([niconico_post.id, second_niconico_post.id, youtube_post.id])
          .to include(theatre.reload.current_post_id)
        expect(theatre.reload.current_post_started_at)
          .to be_within(1.second).of(Time.current)
        expect(theatre.programmes.count).to eq(1)
      end
    end

    context 'when only a YouTube post is eligible' do
      before do
        niconico_post.destroy!
        second_niconico_post.destroy!
        theatre.update!(host_user: member)
        sign_in_as(member)
      end

      it 'sets current_post to the YouTube post' do
        do_request

        expect(response).to have_http_status(:no_content)
        expect(theatre.reload.current_post_id).to eq(youtube_post.id)
      end
    end

    context 'when current user is host and no eligible post exists' do
      before do
        niconico_post.destroy!
        second_niconico_post.destroy!
        youtube_post.destroy!
        theatre.update!(
          host_user: member,
          current_post: other_post,
          current_post_started_at: 1.hour.ago
        )
        sign_in_as(member)
      end

      it 'still returns 204 and clears current_post' do
        do_request

        expect(response).to have_http_status(:no_content)

        theatre.reload
        expect(theatre.current_post_id).to be_nil
        expect(theatre.current_post_started_at).to be_nil
      end
    end
  end

  describe 'PUT /theatres/:id/skip_vote' do
    subject(:do_request) do
      put "/theatres/#{theatre.id}/skip_vote", params: { post_id: requested_post_id }
    end

    let(:third_user) { create(:user, :member, name: 'third user') }
    let(:requested_post_id) { niconico_post.id }

    before do
      theatre.update!(current_post: niconico_post, current_post_started_at: 10.seconds.ago)
      [member, other_user, third_user].each do |user|
        TheatreWatchingUser.create!(
          theatre:,
          user:,
          expires_at: 10.seconds.from_now
        )
      end
    end

    it 'returns 401 when not logged in' do
      sign_out

      expect { do_request }.not_to change(TheatreSkipVote, :count)

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

    it 'returns 422 when post_id is invalid' do
      sign_in_as(member)

      expect {
        put "/theatres/#{theatre.id}/skip_vote", params: { post_id: 'invalid' }
      }.not_to change(TheatreSkipVote, :count)

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

    it 'records a vote and returns the current vote status before majority' do
      sign_in_as(member)

      expect { do_request }.to change(TheatreSkipVote, :count).by(1)

      expect(response).to have_http_status(:ok)
      expect(json['skipped']).to eq(false)
      expect(json['post_id']).to eq(niconico_post.id)
      expect(json['skip_vote']).to include(
        'votes_count' => 1,
        'required_count' => 2,
        'watching_users_count' => 3,
        'voted' => true
      )
    end

    it 'finalizes skip when votes reach majority and stores voters and tag snapshots' do
      tag = create(:tag, name: 'skip-target')
      PostTag.create!(post: niconico_post, tag:)

      TheatreSkipVote.create!(theatre:, post: niconico_post, user: member)
      sign_in_as(other_user)

      expect { do_request }
        .to change(TheatreSkipEvent, :count).by(1)
        .and change(TheatreSkipEventVoter, :count).by(2)
        .and change(TheatreSkipEventTag, :count).by(1)

      expect(response).to have_http_status(:ok)
      expect(json['skipped']).to eq(true)
      expect([second_niconico_post.id, youtube_post.id]).to include(json['post_id'])

      event = TheatreSkipEvent.last
      expect(event.post).to eq(niconico_post)
      expect(event.users).to contain_exactly(member, other_user)
      expect(event.tags).to contain_exactly(tag)
      expect(TheatreSkipVote.where(theatre:, post: niconico_post)).to be_empty
    end

    it 'does not record a vote when requested post is no longer current' do
      theatre.update!(current_post: second_niconico_post)
      sign_in_as(member)

      expect { do_request }.not_to change(TheatreSkipVote, :count)

      expect(response).to have_http_status(:conflict)
      expect(json['post_id']).to eq(second_niconico_post.id)
      expect(json['skip_vote']).to include(
        'votes_count' => 0,
        'voted' => false
      )
    end
  end

  describe 'DELETE /theatres/:id/skip_vote' do
    let(:requested_post_id) { niconico_post.id }

    before do
      theatre.update!(current_post: niconico_post, current_post_started_at: 10.seconds.ago)
      TheatreWatchingUser.create!(theatre:, user: member, expires_at: 10.seconds.from_now)
      TheatreSkipVote.create!(theatre:, post: niconico_post, user: member)
      sign_in_as(member)
    end

    it 'removes the current user vote' do
      expect {
        delete "/theatres/#{theatre.id}/skip_vote", params: { post_id: requested_post_id }
      }.to change(TheatreSkipVote, :count).by(-1)

      expect(response).to have_http_status(:ok)
      expect(json['skip_vote']).to include(
        'votes_count' => 0,
        'required_count' => 1,
        'watching_users_count' => 1,
        'voted' => false
      )
    end

    it 'does not remove a vote when requested post is no longer current' do
      theatre.update!(current_post: second_niconico_post)

      expect {
        delete "/theatres/#{theatre.id}/skip_vote", params: { post_id: requested_post_id }
      }.not_to change(TheatreSkipVote, :count)

      expect(response).to have_http_status(:conflict)
      expect(json['post_id']).to eq(second_niconico_post.id)
    end
  end

  describe 'GET /theatres/:id/skip_events' do
    before do
      sign_in_as(member)
    end

    it 'does not expose skip voters' do
      event = TheatreSkipEvent.create!(
        theatre:,
        post: niconico_post,
        skipped_by_user: member,
        created_at: Time.current
      )
      TheatreSkipEventVoter.create!(theatre_skip_event: event, user: member)

      get "/theatres/#{theatre.id}/skip_events"

      expect(response).to have_http_status(:ok)
      expect(json.first).to include(
        'id' => event.id,
        'theatre_id' => theatre.id
      )
      expect(json.first).not_to have_key('voters')
      expect(json.first).not_to have_key('skipped_by_user')
    end
  end

  describe 'GET /theatres/:id/post_selection_weights' do
    before do
      theatre.update!(current_post: niconico_post)
      TheatreWatchingUser.create!(theatre:, user: member, expires_at: 10.seconds.from_now)
      sign_in_as(member)
    end

    it 'returns tag penalties and candidate weights for the current watchers' do
      tag = create(:tag, name: 'heavy-tag')
      PostTag.create!(post: second_niconico_post, tag:)
      event = TheatreSkipEvent.create!(
        theatre:,
        post: niconico_post,
        skipped_by_user: member,
        created_at: Time.current
      )
      TheatreSkipEventVoter.create!(theatre_skip_event: event, user: member)
      TheatreSkipEventTag.create!(theatre_skip_event: event, tag:)

      get "/theatres/#{theatre.id}/post_selection_weights"

      expect(response).to have_http_status(:ok)
      expect(json['tag_penalties'].first['penalty']).to eq(1)
      expect(json['lightest_posts'].first['post']['id']).to eq(second_niconico_post.id)
      expect(json['lightest_posts'].first['penalty']).to eq(1)
    end
  end
end
