require 'rails_helper'

RSpec.describe 'Materials API', type: :request do
  let!(:member_user) { create(:user, :member) }
  let!(:guest_user)  { create(:user) }

  def dummy_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy')
    Rack::Test::UploadedFile.new(StringIO.new(body), type, original_filename: filename)
  end

  def response_materials
    json.fetch('materials')
  end

  def build_material(tag:, user:, parent: nil, file: dummy_upload, url: nil)
    Material.new(tag:, parent:, url:, created_by_user: user, updated_by_user: user).tap do |material|
      material.file.attach(file) if file
      material.save!
    end
  end

  describe 'GET /materials' do
    let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material) }
    let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material) }

    let!(:material_a) do
      build_material(tag: tag_a, user: member_user, file: dummy_upload(filename: 'a.png'))
    end

    let!(:material_b) do
      build_material(tag: tag_b, user: member_user, parent: material_a, file: dummy_upload(filename: 'b.png'))
    end

    before do
      old_time = Time.zone.local(2026, 3, 29, 1, 0, 0)
      new_time = Time.zone.local(2026, 3, 29, 2, 0, 0)

      material_a.update_columns(created_at: old_time, updated_at: old_time)
      material_b.update_columns(created_at: new_time, updated_at: new_time)
    end

    it 'returns materials with count and metadata' do
      get '/materials'

      expect(response).to have_http_status(:ok)
      expect(json).to include('materials', 'count')
      expect(response_materials).to be_an(Array)
      expect(json['count']).to eq(2)

      row = response_materials.find { |m| m['id'] == material_b.id }
      expect(row).to be_present
      expect(row['tag']).to include(
        'id' => tag_b.id,
        'name' => 'material_index_b',
        'category' => 'material'
      )
      expect(row['created_by_user']).to include(
        'id' => member_user.id,
        'name' => member_user.name
      )
      expect(row['content_type']).to eq('image/png')
    end

    it 'filters materials by tag_id' do
      get '/materials', params: { tag_id: material_a.tag_id }

      expect(response).to have_http_status(:ok)
      expect(json['count']).to eq(1)
      expect(response_materials.map { |m| m['id'] }).to eq([material_a.id])
    end

    it 'filters materials by parent_id' do
      get '/materials', params: { parent_id: material_a.id }

      expect(response).to have_http_status(:ok)
      expect(json['count']).to eq(1)
      expect(response_materials.map { |m| m['id'] }).to eq([material_b.id])
    end

    it 'paginates and keeps total count' do
      get '/materials', params: { page: 2, limit: 1 }

      expect(response).to have_http_status(:ok)
      expect(json['count']).to eq(2)
      expect(response_materials.size).to eq(1)
      expect(response_materials.first['id']).to eq(material_a.id)
    end

    it 'normalises invalid page and limit' do
      get '/materials', params: { page: 0, limit: 0 }

      expect(response).to have_http_status(:ok)
      expect(json['count']).to eq(2)
      expect(response_materials.size).to eq(1)
      expect(response_materials.first['id']).to eq(material_b.id)
    end
  end

  describe 'GET /materials/:id' do
    let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material) }
    let!(:material) do
      build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
    end

    it 'returns a material with file, tag, and content_type' do
      get "/materials/#{ material.id }"

      expect(response).to have_http_status(:ok)
      expect(json).to include(
        'id' => material.id,
        'content_type' => 'image/png'
      )
      expect(json['file']).to be_present
      expect(json['tag']).to include(
        'id' => tag.id,
        'name' => 'material_show',
        'category' => 'material'
      )
    end

    it 'returns 404 when material does not exist' do
      get '/materials/999999999'
      expect(response).to have_http_status(:not_found)
    end
  end

  describe 'POST /materials' do
    context 'when not logged in' do
      before { sign_out }

      it 'returns 401' do
        post '/materials', params: {
          tag: 'material_create_unauthorized',
          file: dummy_upload
        }

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

    context 'when logged in' do
      before { sign_in_as(guest_user) }

      it 'returns 422 when tag is blank' do
        post '/materials', params: { tag: '   ', file: dummy_upload }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(json.fetch('errors')).to include(
            'tag' => ['タグは必須です．'])
      end

      it 'returns 422 when both file and url are blank' do
        post '/materials', params: { tag: 'material_create_blank' }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(json.fetch('errors')).to include(
            'file' => ['ファイルまたは URL は必須です．'],
            'url' => ['ファイルまたは URL は必須です．'])
      end

      it 'creates a material with an attached file' do
        expect do
          post '/materials', params: {
            tag: 'material_create_new',
            file: dummy_upload(filename: 'created.png')
          }
        end.to change(Material, :count).by(1)
          .and change(Tag, :count).by(1)
          .and change(TagName, :count).by(1)

        expect(response).to have_http_status(:created)

        material = Material.order(:id).last
        expect(material.tag.name).to eq('material_create_new')
        expect(material.tag.category).to eq('material')
        expect(material.created_by_user).to eq(guest_user)
        expect(material.updated_by_user).to eq(guest_user)
        expect(material.file.attached?).to be(true)

        expect(json['id']).to eq(material.id)
        expect(json.dig('tag', 'name')).to eq('material_create_new')
        expect(json['content_type']).to eq('image/png')
      end

      it 'returns 422 when the existing tag is not material/character' do
        general_tag_name = TagName.create!(name: 'material_create_general_tag')
        Tag.create!(tag_name: general_tag_name, category: :general)

        post '/materials', params: {
          tag: 'material_create_general_tag',
          file: dummy_upload
        }

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

      it 'persists url-only material' do
        expect do
          post '/materials', params: {
            tag: 'material_create_url_only',
            url: 'https://example.com/material-source'
          }
        end.to change(Material, :count).by(1)

        expect(response).to have_http_status(:created)

        material = Material.order(:id).last
        expect(material.tag.name).to eq('material_create_url_only')
        expect(material.url).to eq('https://example.com/material-source')
        expect(material.file.attached?).to be(false)
      end

      it 'returns the original url for url-only material' do
        post '/materials', params: {
          tag: 'material_create_url_only_response',
          url: 'https://example.com/material-source'
        }

        expect(response).to have_http_status(:created)
        expect(json['url']).to eq('https://example.com/material-source')
      end
    end
  end

  describe 'PUT /materials/:id' do
    let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material) }
    let!(:material) do
      build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
    end

    context 'when not logged in' do
      before { sign_out }

      it 'returns 401' do
        put "/materials/#{ material.id }", params: {
          tag: 'material_update_new',
          file: dummy_upload(filename: 'new.png')
        }

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

    context 'when logged in but not member' do
      before { sign_in_as(guest_user) }

      it 'returns 403' do
        put "/materials/#{ material.id }", params: {
          tag: 'material_update_new',
          file: dummy_upload(filename: 'new.png')
        }

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

    context 'when member' do
      before { sign_in_as(member_user) }

      it 'returns 404 when material does not exist' do
        put '/materials/999999999', params: {
          tag: 'material_update_missing',
          file: dummy_upload
        }

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

      it 'returns 422 when tag is blank' do
        put "/materials/#{ material.id }", params: {
          tag: '   ',
          file: dummy_upload
        }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(json.fetch('errors')).to include(
            'tag' => ['タグは必須です．'])
      end

      it 'returns 422 when both file and url are blank' do
        put "/materials/#{ material.id }", params: {
          tag: 'material_update_no_payload'
        }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(json.fetch('errors')).to include(
            'file' => ['ファイルまたは URL は必須です．'],
            'url' => ['ファイルまたは URL は必須です．'])
      end

      it 'updates tag, url, file, and updated_by_user' do
        old_blob_id = material.file.blob.id

        put "/materials/#{ material.id }", params: {
          tag: 'material_update_new',
          url: 'https://example.com/updated-source',
          file: dummy_upload(filename: 'updated.jpg', type: 'image/jpeg')
        }

        expect(response).to have_http_status(:ok)

        material.reload
        expect(material.tag.name).to eq('material_update_new')
        expect(material.tag.category).to eq('material')
        expect(material.url).to eq('https://example.com/updated-source')
        expect(material.updated_by_user).to eq(member_user)
        expect(material.file.attached?).to be(true)
        expect(material.file.blob.id).not_to eq(old_blob_id)
        expect(material.file.blob.filename.to_s).to eq('updated.jpg')
        expect(material.file.blob.content_type).to eq('image/jpeg')

        expect(json['id']).to eq(material.id)
        expect(json['file']).to be_present
        expect(json['content_type']).to eq('image/jpeg')
        expect(json.dig('tag', 'name')).to eq('material_update_new')
      end

      it 'purges the existing file when file is omitted and url is provided' do
        old_blob_id = material.file.blob.id

        put "/materials/#{ material.id }", params: {
          tag: 'material_update_remove_file',
          url: 'https://example.com/updated-source'
        }

        expect(response).to have_http_status(:ok)

        material.reload
        expect(material.tag.name).to eq('material_update_remove_file')
        expect(material.url).to eq('https://example.com/updated-source')
        expect(material.updated_by_user).to eq(member_user)
        expect(material.file.attached?).to be(false)

        expect(
          ActiveStorage::Blob.where(id: old_blob_id).exists?
        ).to be(false)

        expect(json['id']).to eq(material.id)
        expect(json['file']).to be_nil
        expect(json['content_type']).to be_nil
        expect(json.dig('tag', 'name')).to eq('material_update_remove_file')
        expect(json['url']).to eq('https://example.com/updated-source')
      end
    end
  end

  describe 'DELETE /materials/:id' do
    let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material) }
    let!(:material) do
      build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
    end

    context 'when not logged in' do
      before { sign_out }

      it 'returns 401' do
        delete "/materials/#{ material.id }"
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'when logged in but not member' do
      before { sign_in_as(guest_user) }

      it 'returns 403' do
        delete "/materials/#{ material.id }"
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'when member' do
      before { sign_in_as(member_user) }

      it 'returns 404 when material does not exist' do
        delete '/materials/999999999'
        expect(response).to have_http_status(:not_found)
      end

      it 'discards the material and returns 204' do
        delete "/materials/#{ material.id }"

        expect(response).to have_http_status(:no_content)
        expect(Material.find_by(id: material.id)).to be_nil
        expect(Material.with_discarded.find(material.id)).to be_discarded
      end
    end
  end
end
