require 'rails_helper'

RSpec.describe 'Gekanator learning API', type: :request do
  let(:admin) { create(:user, :admin) }
  let(:member) { create(:user, :member) }
  let(:other_user) { create(:user, :member) }

  let!(:guessed_post) do
    Post.create!(
      title: 'guessed',
      url: 'https://example.com/guessed'
    )
  end

  let!(:correct_post) do
    Post.create!(
      title: 'correct',
      url: 'https://example.com/correct'
    )
  end

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

  let!(:game) do
    GekanatorGame.create!(
      user: admin,
      guessed_post: guessed_post,
      correct_post: correct_post,
      won: false,
      question_count: 1,
      answers: [
        {
          'question_id' => 'tag:character:喜多郁代',
          'question_text' => '喜多ちゃんが関係してる?',
          'answer' => 'yes',
          'original_answer' => 'yes'
        }
      ]
    )
  end

  def create_post_similarity_question!(
    text: '喜多ちゃんが泣いてる?',
    post: correct_post,
    answer: 'yes',
    status: 'accepted',
    source: 'user_suggested',
    priority_weight: 1.2
  )
    GekanatorQuestion.create!(
      text: text,
      kind: 'post_similarity',
      source: source,
      status: status,
      priority_weight: priority_weight,
      condition: {
        type: 'post-similarity',
        postId: post.id,
        answer: answer,
        threshold: 0.65
      },
      created_by: admin
    )
  end

  describe 'POST /gekanator/games' do
    it 'stores a game result for an admin user' do
      sign_in_as admin

      post '/gekanator/games', params: {
        guessed_post_id: guessed_post.id,
        correct_post_id: correct_post.id,
        answers: [
          {
            question_id: 'tag:character:喜多郁代',
            question_text: '喜多ちゃんが関係してる?',
            answer: 'yes',
            original_answer: 'yes'
          }
        ]
      }

      expect(response).to have_http_status(:created)

      created = GekanatorGame.find(json['id'])
      expect(created.user).to eq(admin)
      expect(created.guessed_post).to eq(guessed_post)
      expect(created.correct_post).to eq(correct_post)
      expect(created.won).to eq(false)
      expect(created.question_count).to eq(1)
      expect(created.answers).to eq([
        {
          'question_id' => 'tag:character:喜多郁代',
          'question_text' => '喜多ちゃんが関係してる?',
          'answer' => 'yes',
          'original_answer' => 'yes'
        }
      ])
    end

    it 'stores a won game when guessed_post_id equals correct_post_id' do
      sign_in_as admin

      post '/gekanator/games', params: {
        guessed_post_id: correct_post.id,
        correct_post_id: correct_post.id,
        answers: []
      }

      expect(response).to have_http_status(:created)
      expect(GekanatorGame.find(json['id']).won).to eq(true)
    end

    it 'rejects a game without correct_post_id' do
      sign_in_as admin

      expect {
        post '/gekanator/games', params: {
          guessed_post_id: guessed_post.id,
          answers: []
        }
      }.not_to change { GekanatorGame.count }

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

    it 'stores a game result for a non-admin user' do
      sign_in_as member

      post '/gekanator/games', params: {
        guessed_post_id: guessed_post.id,
        correct_post_id: correct_post.id,
        answers: []
      }

      expect(response).to have_http_status(:created)
      expect(GekanatorGame.find(json['id']).user).to eq(member)
    end

    it 'returns unauthorized without a user' do
      post '/gekanator/games', params: {
        guessed_post_id: guessed_post.id,
        correct_post_id: correct_post.id,
        answers: []
      }

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

    it 'learns accepted non-nico tag answers from camelCase main game logs' do
      sign_in_as admin

      tag_question = GekanatorQuestion.create!(
        text: 'MAD 要素がある?',
        kind: 'tag',
        source: 'admin_curated',
        status: 'accepted',
        priority_weight: 1.0,
        condition: { type: 'tag', key: 'meme:MAD' },
        created_by: admin
      )

      expect {
        post '/gekanator/games', params: {
          guessed_post_id: guessed_post.id,
          correct_post_id: correct_post.id,
          answers: [
            {
              questionId: 'tag:meme:MAD',
              question_text: 'MAD 要素がある?',
              answer: 'yes',
              original_answer: 'yes'
            },
            {
              questionId: 'tag:meme:missing',
              question_text: '存在しない質問?',
              answer: 'yes',
              original_answer: 'yes'
            },
            {
              questionId: 'tag:meme:MAD',
              question_text: 'MAD 要素がある?',
              answer: 'unknown',
              original_answer: 'unknown'
            }
          ]
        }
      }.to change { GekanatorQuestionExample.count }.by(1)

      expect(response).to have_http_status(:created)
      expect(json['learned_example_count']).to eq(1)

      example = GekanatorQuestionExample.last
      expect(example).to have_attributes(
        gekanator_question_id: tag_question.id,
        post_id: correct_post.id,
        user_id: admin.id,
        answer: 'yes',
        source: 'post_game_answer'
      )
      expect(example.gekanator_game_id).to eq(json['id'])
    end

    it 'learns accepted post_similarity answers from main game logs' do
      sign_in_as admin

      question = create_post_similarity_question!(text: '泣いてる?')

      expect {
        post '/gekanator/games', params: {
          guessed_post_id: guessed_post.id,
          correct_post_id: correct_post.id,
          answers: [
            {
              question_id: "post-similarity:#{question.id}",
              question_text: '泣いてる?',
              answer: 'partial',
              original_answer: 'partial'
            }
          ]
        }
      }.to change { GekanatorQuestionExample.count }.by(1)

      expect(response).to have_http_status(:created)
      expect(json['learned_example_count']).to eq(1)

      example = GekanatorQuestionExample.last
      expect(example).to have_attributes(
        gekanator_question_id: question.id,
        post_id: correct_post.id,
        user_id: admin.id,
        answer: 'partial',
        source: 'post_game_answer'
      )
      expect(example.gekanator_game_id).to eq(json['id'])
    end

    it 'does not learn fact questions or nico tag questions from main game logs' do
      sign_in_as admin

      [
        {
          text: 'example.com 由来?',
          kind: 'source',
          condition: { type: 'source', host: 'example.com' }
        },
        {
          text: '題名に結束バンドを含む?',
          kind: 'title',
          condition: { type: 'title-contains', text: '結束バンド' }
        },
        {
          text: '2024 年投稿?',
          kind: 'original_date',
          condition: { type: 'original-year', year: 2024 }
        },
        {
          text: 'ニコニコにぼっちタグ?',
          kind: 'tag',
          condition: { type: 'tag', key: 'nico:ぼっち' }
        }
      ].each do |attributes|
        GekanatorQuestion.create!(
          text: attributes[:text],
          kind: attributes[:kind],
          source: 'admin_curated',
          status: 'accepted',
          priority_weight: 1.0,
          condition: attributes[:condition],
          created_by: admin
        )
      end

      expect {
        post '/gekanator/games', params: {
          guessed_post_id: guessed_post.id,
          correct_post_id: correct_post.id,
          answers: [
            { question_id: 'source:example.com', answer: 'yes' },
            { question_id: 'title:contains:結束バンド', answer: 'yes' },
            { question_id: 'original-year:2024', answer: 'yes' },
            { question_id: 'tag:nico:ぼっち', answer: 'yes' }
          ]
        }
      }.not_to change { GekanatorQuestionExample.count }

      expect(response).to have_http_status(:created)
      expect(json['learned_example_count']).to eq(0)
    end

    it 'updates an existing main game example instead of duplicating it' do
      sign_in_as admin

      tag_question = GekanatorQuestion.create!(
        text: '喜多ちゃんが関係してる?',
        kind: 'tag',
        source: 'admin_curated',
        status: 'accepted',
        priority_weight: 1.0,
        condition: { type: 'tag', key: 'character:喜多郁代' },
        created_by: admin
      )
      existing = GekanatorQuestionExample.create!(
        gekanator_question: tag_question,
        post: correct_post,
        user: admin,
        answer: 'no',
        source: 'post_game_answer',
        weight: 1.0
      )

      expect {
        post '/gekanator/games', params: {
          guessed_post_id: guessed_post.id,
          correct_post_id: correct_post.id,
          answers: [
            {
              question_id: 'tag:character:喜多郁代',
              answer: 'yes',
              original_answer: 'yes'
            }
          ]
        }
      }.not_to change { GekanatorQuestionExample.count }

      expect(response).to have_http_status(:created)
      expect(json['learned_example_count']).to eq(1)
      expect(existing.reload.answer).to eq('yes')
      expect(existing.sample_count).to eq(2)
    end
  end

  describe 'POST /gekanator/question_suggestions' do
    it 'creates a suggestion and promotes yes answer to an accepted post_similarity question' do
      sign_in_as admin

      expect {
        post '/gekanator/question_suggestions', params: {
          gekanator_game_id: game.id,
          question_text: '喜多ちゃんが泣いてる?',
          answer: 'yes'
        }
      }.to change { GekanatorQuestionSuggestion.count }.by(1)
        .and change { GekanatorQuestion.count }.by(1)
        .and change { GekanatorQuestionExample.count }.by(1)

      expect(response).to have_http_status(:created)

      suggestion = GekanatorQuestionSuggestion.last
      question = GekanatorQuestion.last
      example = GekanatorQuestionExample.last

      expect(json).to include(
        'id' => suggestion.id,
        'count' => 1
      )

      expect(suggestion).to have_attributes(
        gekanator_game_id: game.id,
        user_id: admin.id,
        question_text: '喜多ちゃんが泣いてる?',
        answer: 'yes',
        processed: true
      )

      expect(question).to have_attributes(
        text: '喜多ちゃんが泣いてる?',
        kind: 'post_similarity',
        source: 'user_suggested',
        status: 'accepted',
        priority_weight: 1.2,
        gekanator_question_suggestion_id: suggestion.id,
        created_by_id: admin.id
      )
      expect(question.condition).to include(
        'type' => 'post-similarity',
        'postId' => correct_post.id,
        'answer' => 'yes',
        'threshold' => 0.65
      )

      expect(example).to have_attributes(
        gekanator_question_id: question.id,
        post_id: correct_post.id,
        user_id: admin.id,
        gekanator_game_id: game.id,
        answer: 'yes',
        source: 'initial_suggestion',
        weight: 1.0
      )
    end

    it 'promotes no, partial, and probably_no answers' do
      sign_in_as admin

      ['no', 'partial', 'probably_no'].each do |answer|
        expect {
          post '/gekanator/question_suggestions', params: {
            gekanator_game_id: game.id,
            question_text: "answer #{answer} question?",
            answer: answer
          }
        }.to change { GekanatorQuestion.count }.by(1)
          .and change { GekanatorQuestionExample.count }.by(1)

        expect(response).to have_http_status(:created)
        expect(GekanatorQuestion.last.condition['answer']).to eq(answer)
        expect(GekanatorQuestionExample.last.answer).to eq(answer)
      end
    end

    it 'does not promote unknown answers' do
      sign_in_as admin

      expect {
        post '/gekanator/question_suggestions', params: {
          gekanator_game_id: game.id,
          question_text: 'よく分からない質問?',
          answer: 'unknown'
        }
      }.to change { GekanatorQuestionSuggestion.count }.by(1)
        .and change { GekanatorQuestion.count }.by(0)
        .and change { GekanatorQuestionExample.count }.by(0)

      expect(response).to have_http_status(:created)
      expect(GekanatorQuestionSuggestion.last.processed).to eq(false)
    end

    it 'allows more than three suggestions per game' do
      sign_in_as admin

      3.times do |i|
        GekanatorQuestionSuggestion.create!(
          gekanator_game: game,
          user: admin,
          question_text: "existing question #{i}",
          answer: 'unknown'
        )
      end

      expect {
        post '/gekanator/question_suggestions', params: {
          gekanator_game_id: game.id,
          question_text: 'fourth question?',
          answer: 'yes'
        }
      }.to change { GekanatorQuestionSuggestion.count }.by(1)

      expect(response).to have_http_status(:created)
      expect(json['count']).to eq(4)
    end

    it 'allows a non-admin user to suggest a question for their own game' do
      member_game = GekanatorGame.create!(
        user: member,
        guessed_post: guessed_post,
        correct_post: correct_post,
        won: false,
        question_count: 1,
        answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
      )
      sign_in_as member

      expect {
        post '/gekanator/question_suggestions', params: {
          gekanator_game_id: member_game.id,
          question_text: 'member question?',
          answer: 'yes'
        }
      }.to change { GekanatorQuestionSuggestion.count }.by(1)

      expect(response).to have_http_status(:created)
      expect(GekanatorQuestionSuggestion.last).to have_attributes(
        gekanator_game_id: member_game.id,
        user_id: member.id
      )
    end

    it 'returns not found for another user game' do
      sign_in_as member

      expect {
        post '/gekanator/question_suggestions', params: {
          gekanator_game_id: game.id,
          question_text: 'member question?',
          answer: 'yes'
        }
      }.not_to change { GekanatorQuestionSuggestion.count }

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

    it 'returns unauthorized without a user' do
      expect {
        post '/gekanator/question_suggestions', params: {
          gekanator_game_id: game.id,
          question_text: 'member question?',
          answer: 'yes'
        }
      }.not_to change { GekanatorQuestionSuggestion.count }

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

  describe 'GET /gekanator/games/:id/extra_questions' do
    it 'returns at most six accepted user_suggested post_similarity questions without duplicates' do
      sign_in_as admin

      lowest = create_post_similarity_question!(
        text: 'lowest?',
        priority_weight: 0.5
      )
      low = create_post_similarity_question!(
        text: 'low?',
        priority_weight: 1.0
      )
      middle = create_post_similarity_question!(
        text: 'middle?',
        priority_weight: 1.5
      )
      medium_high = create_post_similarity_question!(
        text: 'medium high?',
        priority_weight: 2.0
      )
      high = create_post_similarity_question!(
        text: 'high?',
        priority_weight: 2.5
      )
      higher = create_post_similarity_question!(
        text: 'higher?',
        priority_weight: 2.8
      )
      highest = create_post_similarity_question!(
        text: 'highest?',
        priority_weight: 3.0
      )
      overflow = create_post_similarity_question!(
        text: 'overflow?',
        priority_weight: 2.2
      )

      get "/gekanator/games/#{game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].length).to eq(6)
      expect(json['questions'].map { _1['id'] }.uniq.length).to eq(6)
      expect(json['questions'].map { _1['id'] }).to all(
        be_in([
          lowest.id,
          low.id,
          middle.id,
          medium_high.id,
          high.id,
          higher.id,
          highest.id,
          overflow.id,
        ])
      )
    end

    it 'can return questions that already have an example for the correct post' do
      sign_in_as admin

      existing = create_post_similarity_question!(text: 'already learned?')

      GekanatorQuestionExample.create!(
        gekanator_question: existing,
        post: correct_post,
        user: admin,
        gekanator_game: game,
        answer: 'yes',
        source: 'post_game_extra'
      )

      get "/gekanator/games/#{game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].map { _1['id'] }).to include(existing.id)
    end

    it 'prioritizes questions the current user has not answered' do
      sign_in_as admin

      answered = create_post_similarity_question!(
        text: 'already answered?',
        priority_weight: 3.0
      )
      GekanatorQuestionExample.create!(
        gekanator_question: answered,
        post: other_post,
        user: admin,
        answer: 'yes',
        source: 'post_game_extra'
      )

      unanswered =
          6.times.map { |index|
            create_post_similarity_question!(
              text: "unanswered #{index}?",
              priority_weight: 0.5
            )
          }

      get "/gekanator/games/#{game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].map { _1['id'] }).to match_array(
        unanswered.map(&:id)
      )
    end

    it 'can return questions already asked in the game using snake_case question_id' do
      sign_in_as admin

      asked = create_post_similarity_question!(text: 'already asked?')
      game.update!(
        answers: [
          {
            'question_id' => "post-similarity:#{asked.id}",
            'answer' => 'yes'
          }
        ]
      )

      get "/gekanator/games/#{game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].map { _1['id'] }).to include(asked.id)
    end

    it 'can return questions already asked in the game using camelCase questionId' do
      sign_in_as admin

      asked = create_post_similarity_question!(text: 'already asked?')
      game.update!(
        answers: [
          {
            'questionId' => "post-similarity:#{asked.id}",
            'answer' => 'yes'
          }
        ]
      )

      get "/gekanator/games/#{game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].map { _1['id'] }).to include(asked.id)
    end

    it 'does not return non-accepted, non-user_suggested, or non-post_similarity questions' do
      sign_in_as admin

      accepted = create_post_similarity_question!(text: 'accepted?')
      create_post_similarity_question!(text: 'disabled?', status: 'disabled')
      create_post_similarity_question!(text: 'ai?', source: 'ai_generated')
      GekanatorQuestion.create!(
        text: 'tag?',
        kind: 'tag',
        source: 'user_suggested',
        status: 'accepted',
        priority_weight: 1.0,
        condition: { type: 'tag', key: 'character:喜多郁代' }
      )

      get "/gekanator/games/#{game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].map { _1['id'] }).to contain_exactly(accepted.id)
    end

    it 'allows a non-admin user to fetch extra questions for their own game' do
      member_game = GekanatorGame.create!(
        user: member,
        guessed_post: guessed_post,
        correct_post: correct_post,
        won: false,
        question_count: 1,
        answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
      )
      accepted = create_post_similarity_question!(text: 'accepted?')
      sign_in_as member

      get "/gekanator/games/#{member_game.id}/extra_questions"

      expect(response).to have_http_status(:ok)
      expect(json['questions'].map { _1['id'] }).to include(accepted.id)
    end

    it 'returns not found for another user game' do
      sign_in_as member

      get "/gekanator/games/#{game.id}/extra_questions"

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

    it 'returns unauthorized without a user' do
      get "/gekanator/games/#{game.id}/extra_questions"

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

  describe 'POST /gekanator/games/:id/extra_question_answers' do
    it 'creates examples for extra question answers' do
      sign_in_as admin

      question = create_post_similarity_question!(text: 'extra?')

      expect {
        post "/gekanator/games/#{game.id}/extra_question_answers", params: {
          answers: [
            {
              question_id: question.id.to_s,
              answer: 'partial'
            }
          ]
        }
      }.to change { GekanatorQuestionExample.count }.by(1)

      expect(response).to have_http_status(:created)
      expect(json).to include('count' => 1)

      example = GekanatorQuestionExample.last
      expect(example).to have_attributes(
        gekanator_question_id: question.id,
        post_id: correct_post.id,
        user_id: admin.id,
        gekanator_game_id: game.id,
        answer: 'partial',
        source: 'post_game_extra',
        weight: 1.0
      )
    end

    it 'updates an existing example for the same question, post, and user' do
      sign_in_as admin

      question = create_post_similarity_question!(text: 'extra?')
      existing = GekanatorQuestionExample.create!(
        gekanator_question: question,
        post: correct_post,
        user: admin,
        answer: 'no',
        source: 'post_game_extra',
        weight: 1.0
      )

      expect {
        post "/gekanator/games/#{game.id}/extra_question_answers", params: {
          answers: [
            {
              question_id: question.id,
              answer: 'yes'
            }
          ]
        }
      }.not_to change { GekanatorQuestionExample.count }

      expect(response).to have_http_status(:created)
      expect(existing.reload).to have_attributes(
        answer: 'yes',
        source: 'post_game_extra',
        gekanator_game_id: game.id
      )
    end

    it 'rejects missing questions' do
      sign_in_as admin

      expect {
        post "/gekanator/games/#{game.id}/extra_question_answers", params: {
          answers: [
            {
              question_id: 999_999_999,
              answer: 'yes'
            }
          ]
        }
      }.not_to change { GekanatorQuestionExample.count }

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

    it 'rejects non-accepted questions' do
      sign_in_as admin

      question = create_post_similarity_question!(
        text: 'disabled?',
        status: 'disabled'
      )

      post "/gekanator/games/#{game.id}/extra_question_answers", params: {
        answers: [
          {
            question_id: question.id,
            answer: 'yes'
          }
        ]
      }

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

    it 'rejects non-post_similarity questions' do
      sign_in_as admin

      question = GekanatorQuestion.create!(
        text: 'tag?',
        kind: 'tag',
        source: 'user_suggested',
        status: 'accepted',
        priority_weight: 1.0,
        condition: { type: 'tag', key: 'character:喜多郁代' }
      )

      post "/gekanator/games/#{game.id}/extra_question_answers", params: {
        answers: [
          {
            question_id: question.id,
            answer: 'yes'
          }
        ]
      }

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

    it 'allows a non-admin user to answer extra questions for their own game' do
      member_game = GekanatorGame.create!(
        user: member,
        guessed_post: guessed_post,
        correct_post: correct_post,
        won: false,
        question_count: 1,
        answers: [{ 'question_id' => 'tag:1', 'answer' => 'yes' }]
      )
      question = create_post_similarity_question!(text: 'extra?')
      sign_in_as member

      expect {
        post "/gekanator/games/#{member_game.id}/extra_question_answers", params: {
          answers: [
            {
              question_id: question.id,
              answer: 'yes'
            }
          ]
        }
      }.to change { GekanatorQuestionExample.count }.by(1)

      expect(response).to have_http_status(:created)
      expect(GekanatorQuestionExample.last).to have_attributes(
        user_id: member.id,
        gekanator_game_id: member_game.id
      )
    end

    it 'returns not found for another user game' do
      question = create_post_similarity_question!(text: 'extra?')
      sign_in_as member

      expect {
        post "/gekanator/games/#{game.id}/extra_question_answers", params: {
          answers: [
            {
              question_id: question.id,
              answer: 'yes'
            }
          ]
        }
      }.not_to change { GekanatorQuestionExample.count }

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

    it 'returns unauthorized without a user' do
      question = create_post_similarity_question!(text: 'extra?')

      post "/gekanator/games/#{game.id}/extra_question_answers", params: {
        answers: [
          {
            question_id: question.id,
            answer: 'yes'
          }
        ]
      }

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

  describe 'GET /gekanator/questions' do
    it 'omits questions for deprecated tags' do
      active_tag = Tag.create!(name: 'active_question_tag', category: :general)
      deprecated_tag = Tag.create!(
        name: 'deprecated_question_tag',
        category: :general,
        deprecated_at: Time.current
      )

      [active_tag, deprecated_tag].each do |question_tag|
        GekanatorQuestion.create!(
          text: "#{ question_tag.name }?",
          kind: 'tag',
          source: 'admin_curated',
          status: 'accepted',
          priority_weight: 1.0,
          condition: {
            type: 'tag',
            key: "#{ question_tag.category }:#{ question_tag.name }"
          },
          created_by: admin
        )
      end

      get '/gekanator/questions'

      expect(response).to have_http_status(:ok)
      question_ids = json.fetch('questions').map { |question| question.fetch('id') }
      expect(question_ids).to include('tag:general:active_question_tag')
      expect(question_ids).not_to include('tag:general:deprecated_question_tag')
    end

    it 'returns accepted questions only and includes example_answers for post_similarity questions' do
      sign_in_as admin

      accepted = create_post_similarity_question!(text: 'accepted?')
      create_post_similarity_question!(
        text: 'disabled?',
        status: 'disabled'
      )

      GekanatorQuestionExample.create!(
        gekanator_question: accepted,
        post: correct_post,
        user: admin,
        answer: 'yes',
        source: 'initial_suggestion',
        weight: 1.0
      )

      get '/gekanator/questions'

      expect(response).to have_http_status(:ok)
      expect(json['questions'].length).to eq(1)

      question_json = json['questions'].first
      expect(question_json).to include(
        'id' => "post-similarity:#{accepted.id}",
        'text' => 'accepted?',
        'kind' => 'post_similarity',
        'source' => 'user_suggested',
        'priority_weight' => 1.2
      )
      expect(question_json['condition']).to include(
        'type' => 'post-similarity',
        'postId' => correct_post.id,
        'answer' => 'yes',
        'threshold' => 0.65
      )
      expect(question_json['example_answers']).to include(
        correct_post.id.to_s => 'yes'
      )
    end

    it 'aggregates example_answers by weight' do
      sign_in_as admin

      question = create_post_similarity_question!(text: 'weighted?')

      GekanatorQuestionExample.create!(
        gekanator_question: question,
        post: other_post,
        user: admin,
        answer: 'yes',
        source: 'post_game_extra',
        weight: 1.0
      )
      GekanatorQuestionExample.create!(
        gekanator_question: question,
        post: other_post,
        user: other_user,
        answer: 'no',
        source: 'post_game_extra',
        weight: 2.0
      )

      get '/gekanator/questions'

      expect(response).to have_http_status(:ok)
      question_json = json['questions'].find { _1['id'] == "post-similarity:#{question.id}" }
      expect(question_json['example_answers']).to include(
        other_post.id.to_s => 'no'
      )
    end

    it 'normalizes legacy title length questions' do
      sign_in_as admin

      GekanatorQuestion.create!(
        text: '題名が長めの投稿?',
        kind: 'title',
        source: 'admin_curated',
        status: 'accepted',
        priority_weight: 1.0,
        condition: {
          type: 'title-length-greater-than',
          length: 20
        },
        created_by: admin
      )

      get '/gekanator/questions'

      expect(response).to have_http_status(:ok)
      question_json = json['questions'].find { _1['id'] == 'title:length-at-least:21' }
      expect(question_json).to include(
        'text' => 'タイトルは 21 文字以上?',
        'kind' => 'title'
      )
      expect(question_json['condition']).to include(
        'type' => 'title-length-at-least',
        'length' => 21
      )
    end

    it 'returns title-contains questions without authentication' do
      GekanatorQuestion.create!(
        text: '題名に「結束バンド」が含まれる?',
        kind: 'title',
        source: 'ai_generated',
        status: 'accepted',
        priority_weight: 0.95,
        condition: {
          type: 'title-contains',
          text: '結束バンド'
        },
        created_by: admin
      )

      get '/gekanator/questions'

      expect(response).to have_http_status(:ok)
      question_json = json['questions'].find { _1['id'] == 'title:contains:結束バンド' }
      expect(question_json).to include(
        'text' => '題名に「結束バンド」が含まれる?',
        'kind' => 'title'
      )
      expect(question_json['condition']).to include(
        'type' => 'title-contains',
        'text' => '結束バンド'
      )
    end
  end
end
