# frozen_string_literal: true

require 'rspec'

require 'image_size'
require 'image_size/uri_reader'

require 'tempfile'
require 'shellwords'

require 'test_server'

RSpec.configure do |config|
  config.order = :random
end

class RestrictedIO
  def initialize(data)
    @io = StringIO.new(data)
  end

  def read(length = nil)
    @io.read(length)
  end

  def eof?
    @io.eof?
  end
end

describe ImageSize do
  before :all do
    @server = TestServer.new
  end

  after :all do
    @server.finish
  end

  def retry_on(exception_class)
    attempt = 1
    begin
      yield
    rescue exception_class => e
      warn "Attempt #{attempt}: #{e.inspect}"
      raise unless attempt < 3

      attempt += 1
      retry
    end
  end

  def supported_formats
    ImageSize.private_instance_methods.map{ |name| name[/\Asize_of_(.*)\z/, 1] }.compact.sort
  end

  describe 'README' do
    let(:readme){ File.read('README.markdown') }

    it 'lists all supported formats' do
      expect(readme[/^Formats: .*$/]).to eq("Formats: #{supported_formats.map{ |format| "`#{format}`" }.join(', ')}.")
    end
  end

  describe 'gemspec' do
    let(:gemspec){ Gem::Specification.load('image_size.gemspec') }

    it 'lists all supported formats in description' do
      expect(gemspec.description).to eq("Measure following file dimensions: #{supported_formats.join(', ')}")
    end
  end

  def self.test_image_size(path, &block)
    file_size = File.size(path)
    max_file_size = 16_384
    fail "reduce resulting gem size, #{path} is too big (#{file_size} > #{max_file_size})" if file_size > max_file_size

    describe "for #{path}" do
      let(:name){ File.basename(path) }
      let(:match){ name.match(/(?<width>\d+)x(?<height>\d+)(?:@(?<scale>\d)x)?\.(?<format>[^.]+)$/) }
      let(:scale){ (match[:scale] || 1).to_i if match }
      let(:attributes) do
        if match
          width = match[:width].to_i * scale
          height = match[:height].to_i * scale
          format = match[:format].to_sym
        end
        size = [width, height] if format
        media_types = ImageSize::MEDIA_TYPES[format] || []
        media_type = format && media_types.first.to_s
        {
          format: format,
          width: width,
          height: height,
          w: width,
          h: height,
          size: size,
          media_type: media_type,
          media_types: media_types,
          byte_size: file_size,
        }
      end
      let(:file_data){ File.binread(path) }

      instance_exec(&block) if block

      context 'given as data' do
        it 'gets format and dimensions' do
          data = file_data.dup
          image_size = ImageSize.new(data)
          expect(image_size).to have_attributes(attributes)
          expect(data).to eq(file_data)
        end
      end

      context 'given as IO' do
        it 'gets format and dimensions' do
          File.open(path, 'rb') do |io|
            image_size = ImageSize.new(io)
            expect(image_size).to have_attributes(attributes)
            expect(io).not_to be_closed
            if file_size.zero?
              expect(io.pos).to be_zero
            else
              expect(io.pos).to_not be_zero
            end
            io.rewind
            expect(io.read).to eq(file_data)
          end
        end
      end

      context 'given as unseekable IO' do
        let(:attributes){ super().merge(byte_size: nil) }

        it 'gets format and dimensions' do
          IO.popen(%W[cat #{path}].shelljoin, 'rb') do |io|
            image_size = ImageSize.new(io)
            expect(image_size).to have_attributes(attributes)
            expect(io).not_to be_closed
          end
        end
      end

      context 'given an object allowing only read and eof?' do
        let(:attributes){ super().merge(byte_size: nil) }

        it 'gets format and dimensions' do
          io = RestrictedIO.new(file_data)
          image_size = ImageSize.new(io)
          expect(image_size).to have_attributes(attributes)
        end
      end

      context 'given as StringIO' do
        it 'gets format and dimensions' do
          io = StringIO.new(file_data)
          image_size = ImageSize.new(io)
          expect(image_size).to have_attributes(attributes)
          expect(io).not_to be_closed
          io.rewind
          expect(io.read).to eq(file_data)
        end
      end

      context 'given as Tempfile' do
        it 'gets format and dimensions' do
          Tempfile.open(name) do |io|
            io.binmode
            io.write(file_data)
            io.rewind
            image_size = ImageSize.new(io)
            expect(image_size).to have_attributes(attributes)
            expect(io).not_to be_closed
            if file_size.zero?
              expect(io.pos).to be_zero
            else
              expect(io.pos).to_not be_zero
            end
            io.rewind
            expect(io.read).to eq(file_data)
          end
        end
      end

      context 'using path method' do
        it 'gets format and dimensions' do
          image_size = ImageSize.path(path)
          expect(image_size).to have_attributes(attributes)
        end
      end

      context 'fetching from webserver' do
        let(:url){ "#{@server.base_url}#{path}#{query}" }

        before{ ImageSize.chunk_size = 64 }
        after{ ImageSize.chunk_size = nil }

        subject do
          retry_on Timeout::Error do
            ImageSize.url(url)
          end
        end

        context 'supporting range' do
          context 'without redirects' do
            let(:query){ '' }

            it 'gets format and dimensions' do
              is_expected.to have_attributes(attributes)
            end
          end

          context 'with redirects' do
            let(:query){ '?redirect=5' }

            it 'gets format and dimensions' do
              is_expected.to have_attributes(attributes)
            end
          end

          context 'with too many redirects' do
            let(:query){ '?redirect=6' }

            it 'raises error' do
              expect{ subject }.to raise_error(/Too many redirects/)
            end
          end

          context 'with unknown file size' do
            let(:query){ '?unknown_file_size' }
            let(:attributes){ super().merge(byte_size: file_size.zero? ? 0 : nil) }

            it 'gets format and dimensions' do
              is_expected.to have_attributes(attributes)
            end
          end
        end

        context 'not supporting range' do
          let(:query){ '?ignore_range' }

          context 'without redirects' do
            it 'gets format and dimensions' do
              is_expected.to have_attributes(attributes)
            end
          end

          context 'with redirects' do
            let(:query){ '?ignore_range&redirect=5' }

            it 'gets format and dimensions' do
              is_expected.to have_attributes(attributes)
            end
          end

          context 'with too many redirects' do
            let(:query){ '?ignore_range&redirect=6' }

            it 'raises error' do
              expect{ subject }.to raise_error(/Too many redirects/)
            end
          end
        end
      end
    end
  end

  context 'for non images' do
    test_image_size 'spec/test_server.rb'
    test_image_size 'spec/images/empty'
  end

  context 'for images' do
    Dir['spec/images/*/*.*'].each do |path|
      if path.end_with?('.icns')
        context 'without use_display_pixels' do
          test_image_size path
        end

        context 'with use_display_pixels' do
          before{ ImageSize.use_display_pixels = true }
          after{ ImageSize.use_display_pixels = nil }

          test_image_size path do
            let(:scale){ 1 }
          end
        end
      else
        test_image_size path
      end
    end
  end

  it 'raises ArgumentError if argument is not valid' do
    expect do
      ImageSize.new(Object)
    end.to raise_error(ArgumentError)
  end

  {
    png: "\211PNG\r\n\032\n",
    jpeg: "\377\330",
  }.each do |type, data|
    it "raises FormatError if invalid #{type} given" do
      expect do
        ImageSize.new(data)
      end.to raise_error(ImageSize::FormatError)
    end
  end

  describe '.dpi' do
    subject{ ImageSize.dpi }

    after{ ImageSize.dpi = nil }

    it 'is 72 by default' do
      is_expected.to eql(72.0)
    end

    it 'is can be set and get converted to Float' do
      ImageSize.dpi = 42

      is_expected.to eql(42.0)
    end

    it 'is can not be set to 0' do
      expect{ ImageSize.dpi = 0 }.to raise_error(ArgumentError)

      is_expected.to eql(72.0)
    end

    it 'is can not be set to a negative value' do
      expect{ ImageSize.dpi = -42 }.to raise_error(ArgumentError)

      is_expected.to eql(72.0)
    end

    it 'can be reset' do
      ImageSize.dpi = 42
      ImageSize.dpi = nil

      is_expected.to eql(72.0)
    end
  end

  describe '.chunk_size' do
    subject{ ImageSize.chunk_size }

    after{ ImageSize.chunk_size = nil }

    it 'is 4096 by default' do
      is_expected.to eql(4096)
    end

    it 'is can be set to an Integer' do
      ImageSize.chunk_size = 256

      is_expected.to eql(256)
    end

    it 'is can not be set to 0' do
      expect{ ImageSize.chunk_size = 0 }.to raise_error(ArgumentError)

      is_expected.to eql(4096)
    end

    it 'is can not be set to a negative value' do
      expect{ ImageSize.chunk_size = -1 }.to raise_error(ArgumentError)

      is_expected.to eql(4096)
    end

    it 'is can not be set to a Float' do
      expect{ ImageSize.chunk_size = 3.5 }.to raise_error(ArgumentError)

      is_expected.to eql(4096)
    end

    it 'can be reset' do
      ImageSize.chunk_size = 256
      ImageSize.chunk_size = nil

      is_expected.to eql(4096)
    end
  end

  describe '.max_redirects' do
    subject{ ImageSize.max_redirects }

    after{ ImageSize.max_redirects = nil }

    it 'is 4096 by default' do
      is_expected.to eql(5)
    end

    it 'is can be set to an Integer' do
      ImageSize.max_redirects = 3

      is_expected.to eql(3)
    end

    it 'is can be set to 0' do
      ImageSize.max_redirects = 0

      is_expected.to eql(0)
    end

    it 'is can not be set to a negative value' do
      expect{ ImageSize.max_redirects = -1 }.to raise_error(ArgumentError)

      is_expected.to eql(5)
    end

    it 'is can not be set to a Float' do
      expect{ ImageSize.max_redirects = 3.5 }.to raise_error(ArgumentError)

      is_expected.to eql(5)
    end

    it 'can be reset' do
      ImageSize.max_redirects = 3
      ImageSize.max_redirects = nil

      is_expected.to eql(5)
    end
  end

  describe '.uri_checker' do
    before{ ImageSize.chunk_size = 64 }
    after do
      ImageSize.chunk_size = nil
      ImageSize.uri_checker = nil
    end

    it 'can be reset to the default checker' do
      ImageSize.uri_checker = proc{ |uri| fail 'forbidden' if uri.port == @server.base_url.port }
      ImageSize.uri_checker = nil

      expect do
        retry_on Timeout::Error do
          ImageSize.url("#{@server.base_url}spec/images/empty")
        end
      end.not_to raise_error
    end

    it 'requires an object responding to call' do
      expect{ ImageSize.uri_checker = Object.new }.to raise_error(ArgumentError)
    end

    it 'is checked before initial request' do
      ImageSize.uri_checker = proc{ |uri| fail 'forbidden' if uri.port == @server.base_url.port }

      expect do
        retry_on Timeout::Error do
          ImageSize.url("#{@server.base_url}spec/images/empty")
        end
      end.to raise_error('forbidden')
    end

    it 'is checked before redirect request' do
      ImageSize.uri_checker = proc{ |uri| fail 'forbidden' if uri.port == @server.second_url.port }

      expect do
        retry_on Timeout::Error do
          ImageSize.url("#{@server.base_url}spec/images/empty?redirect=1")
        end
      end.to raise_error('forbidden')
    end
  end
end
