require 'rails_helper'
require 'set'


RSpec.describe 'Posts API', type: :request do
  # create / update で thumbnail.attach は走るが、
  # resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。
  before do
    allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true)
  end

  def dummy_upload
    # 中身は何でもいい（加工処理はスタブしてる）
    Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
  end

  let!(:tag_name) { TagName.create!(name: 'spec_tag') }
  let!(:tag)      { Tag.create!(tag_name: tag_name, category: 'general') }

  let!(:post_record) do
    Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
      PostTag.create!(post: p, tag: tag)
    end
  end

  describe "GET /posts" do
    let!(:user) { create_member_user! }

    let!(:tag_name) { TagName.create!(name: "spec_tag") }
    let!(:tag) { Tag.create!(tag_name:, category: "general") }
    let!(:tag_name2) { TagName.create!(name: 'unko') }
    let!(:tag2) { Tag.create!(tag_name: tag_name2, category: 'deerjikist') }
    let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }

    let!(:hit_post) do
      Post.create!(uploaded_user: user, title: "hello spec world",
                   url: 'https://example.com/spec2').tap do |p|
        PostTag.create!(post: p, tag:)
      end
    end

    let!(:miss_post) do
      Post.create!(uploaded_user: user, title: "unrelated title",
                   url: 'https://example.com/spec3').tap do |p|
        PostTag.create!(post: p, tag: tag2)
      end
    end

    it "returns posts with tag name in JSON" do
      get "/posts"

      expect(response).to have_http_status(:ok)
      posts = json.fetch("posts")

      # 全postの全tagが name を含むこと
      expect(posts).not_to be_empty
      posts.each do |p|
        expect(p['tags']).to be_an(Array)
        p['tags'].each do |t|
          expect(t).to include('name', 'category', 'has_wiki')
        end
      end
      expect(json['count']).to be_an(Integer)

      # spec_tag を含む投稿が存在すること
      all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } }
      expect(all_tag_names).to include("spec_tag")
    end

    context "when q is provided" do
      it "filters posts by q (hit case)" do
        get "/posts", params: { tags: "spec_tag" }

        expect(response).to have_http_status(:ok)
        posts = json.fetch('posts')
        ids = posts.map { |p| p['id'] }

        expect(ids).to include(hit_post.id)
        expect(ids).not_to include(miss_post.id)
        expect(json['count']).to be_an(Integer)

        posts.each do |p|
          expect(p['tags']).to be_an(Array)
          p['tags'].each do |t|
            expect(t).to include('name', 'category', 'has_wiki')
          end
        end
      end

      it "filters posts by q (hit case by alias)" do
        get "/posts", params: { tags: "manko" }

        expect(response).to have_http_status(:ok)
        posts = json.fetch('posts')
        ids = posts.map { |p| p['id'] }

        expect(ids).to include(hit_post.id)
        expect(ids).not_to include(miss_post.id)
        expect(json['count']).to be_an(Integer)

        posts.each do |p|
          expect(p['tags']).to be_an(Array)
          p['tags'].each do |t|
            expect(t).to include('name', 'category', 'has_wiki')
          end
        end
      end

      it "returns empty posts when nothing matches" do
        get "/posts", params: { tags: "no_such_keyword_12345" }

        expect(response).to have_http_status(:ok)
        expect(json.fetch("posts")).to eq([])
        expect(json.fetch('count')).to eq(0)
      end
    end
  end

  describe 'GET /posts/:id' do
    subject(:request) { get "/posts/#{post_id}" }

    context 'when post exists' do
      let(:post_id) { post_record.id }

      it 'returns post with tag tree + related + viewed' do
        request
        expect(response).to have_http_status(:ok)

        expect(json).to include('id' => post_record.id)
        expect(json).to have_key('tags')
        expect(json['tags']).to be_an(Array)

        # show は build_tag_tree_for を使うので、tags はツリー形式（children 付き）
        node = json['tags'][0]
        expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki')
        expect(node['name']).to eq('spec_tag')

        expect(json).to have_key('related')
        expect(json['related']).to be_an(Array)

        expect(json).to have_key('viewed')
        expect([true, false]).to include(json['viewed'])
      end
    end

    context 'when post does not exist' do
      let(:post_id) { 999_999_999 }

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

  describe 'POST /posts' do
    let(:member) { create(:user, :member) }
    let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }

    it '401 when not logged in' do
      sign_out
      post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
      expect(response).to have_http_status(:unauthorized)
    end

    it '403 when not member' do
      sign_in_as(create(:user, role: 'guest'))
      post '/posts', params: { title: 't', url: 'https://example.com/x', tags: 'a', thumbnail: dummy_upload }
      expect(response).to have_http_status(:forbidden)
    end

    it '201 and creates post + tags when member' do
      sign_in_as(member)

      post '/posts', params: {
        title: 'new post',
        url: 'https://example.com/new',
        tags: 'spec_tag', # 既存タグ名を投げる
        thumbnail: dummy_upload
      }

      expect(response).to have_http_status(:created)
      expect(json).to include('id', 'title', 'url')

      # tags が name を含むこと（API 側の serialization が正しいこと）
      expect(json).to have_key('tags')
      expect(json['tags']).to be_an(Array)
      expect(json['tags'][0]).to have_key('name')
    end

    it '201 and creates post + tags when member and tags have aliases' do
      sign_in_as(member)

      post '/posts', params: {
        title: 'new post',
        url: 'https://example.com/new',
        tags: 'manko', # 既存タグ名を投げる
        thumbnail: dummy_upload
      }

      expect(response).to have_http_status(:created)
      expect(json).to include('id', 'title', 'url')

      # tags が name を含むこと（API 側の serialization が正しいこと）
      names = json.fetch('tags').map { |t| t['name'] }
      expect(names).to include('spec_tag')
      expect(names).not_to include('manko')
    end

    context "when nico tag already exists in tags" do
      before do
        Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
                               category: 'nico')
      end

      it 'return 400' do
        sign_in_as(member)

        post '/posts', params: {
            title: 'new post',
            url: 'https://example.com/nico_tag',
            tags: 'nico:nico_tag',
            thumbnail: dummy_upload }

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

    context 'when url is blank' do
      it 'returns 422' do
        sign_in_as(member)

        post '/posts', params: {
            title: 'new post',
            url: '   ',
            tags: 'spec_tag', # 既存タグ名を投げる
            thumbnail: dummy_upload }

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

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

        post '/posts', params: {
          title: 'new post',
          url: 'ぼざクリタグ広場',
          tags: 'spec_tag', # 既存タグ名を投げる
          thumbnail: dummy_upload
        }

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

  describe 'PUT /posts/:id' do
    let(:member) { create(:user, :member) }

    it '401 when not logged in' do
      sign_out
      put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
      expect(response).to have_http_status(:unauthorized)
    end

    it '403 when not member' do
      sign_in_as(create(:user, role: 'guest'))
      put "/posts/#{post_record.id}", params: { title: 'updated', tags: 'spec_tag' }
      expect(response).to have_http_status(:forbidden)
    end

    it '200 and updates title + resync tags when member' do
      sign_in_as(member)

      # 追加で別タグも作って、更新時に入れ替わることを見る
      tn2 = TagName.create!(name: 'spec_tag_2')
      Tag.create!(tag_name: tn2, category: 'general')

      put "/posts/#{post_record.id}", params: {
        title: 'updated title',
        tags: 'spec_tag_2'
      }

      expect(response).to have_http_status(:ok)
      expect(json).to have_key('tags')
      expect(json['tags']).to be_an(Array)

      # show と同様、update 後レスポンスもツリー形式
      names = json['tags'].map { |n| n['name'] }
      expect(names).to include('spec_tag_2')
    end

    context "when nico tag already exists in tags" do
      before do
        Tag.find_or_create_by!(tag_name: TagName.find_or_create_by!(name: 'nico:nico_tag'),
                               category: 'nico')
      end

      it 'return 400' do
        sign_in_as(member)

        put "/posts/#{ post_record.id }", params: {
            title: 'updated title',
            tags: 'nico:nico_tag' }

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

  describe 'GET /posts/random' do
    it '404 when no posts' do
      PostTag.delete_all
      Post.delete_all
      get '/posts/random'
      expect(response).to have_http_status(:not_found)
    end

    it '200 and returns viewed boolean' do
      get '/posts/random'
      expect(response).to have_http_status(:ok)
      expect(json).to have_key('viewed')
      expect([true, false]).to include(json['viewed'])
    end
  end

  describe 'GET /posts/changes' do
    let(:member) { create(:user, :member) }

    it 'returns add/remove events (history) for a post' do
      # add
      tn2 = TagName.create!(name: 'spec_tag2')
      tag2 = Tag.create!(tag_name: tn2, category: 'general')
      pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)

      # remove (discard)
      pt.discard_by!(member)

      get '/posts/changes', params: { id: post_record.id }

      expect(response).to have_http_status(:ok)
      expect(json).to include('changes', 'count')
      expect(json['changes']).to be_an(Array)
      expect(json['count']).to be >= 2

      types = json['changes'].map { |e| e['change_type'] }.uniq
      expect(types).to include('add')
      expect(types).to include('remove')
    end
  end

  describe 'POST /posts/:id/viewed' do
    let(:user) { create(:user) }

    it '401 when not logged in' do
      sign_out
      post "/posts/#{ post_record.id }/viewed"
      expect(response).to have_http_status(:unauthorized)
    end

    it '204 and marks viewed when logged in' do
      sign_in_as(user)
      post "/posts/#{ post_record.id }/viewed"
      expect(response).to have_http_status(:no_content)

      expect(user.reload.viewed?(post_record)).to be(true)
    end
  end

  describe 'DELETE /posts/:id/viewed' do
    let(:user) { create(:user) }

    it '401 when not logged in' do
      sign_out
      delete "/posts/#{ post_record.id }/viewed"
      expect(response).to have_http_status(:unauthorized)
    end

    it '204 and unmarks viewed when logged in' do
      sign_in_as(user)

      # 先に viewed 付けてから外す
      user.viewed_posts << post_record

      delete "/posts/#{ post_record.id }/viewed"
      expect(response).to have_http_status(:no_content)

      expect(user.reload.viewed?(post_record)).to be(false)
    end
  end
end
