# frozen_string_literal: true

require 'mime/types'
require 'minitest_helper'

describe MIME::Type do
  def mime_type(content_type)
    MIME::Type.new(content_type) { |mt| yield mt if block_given? }
  end

  let(:x_appl_x_zip) {
    mime_type('x-appl/x-zip') { |t| t.extensions = %w(zip zp) }
  }
  let(:text_plain) { mime_type('text/plain') }
  let(:text_html) { mime_type('text/html') }
  let(:image_jpeg) { mime_type('image/jpeg') }
  let(:application_javascript) {
    mime_type('application/javascript') do |js|
      js.friendly('en' => 'JavaScript')
      js.xrefs = {
        'rfc' => %w(rfc4239 rfc4239),
        'template' => %w(application/javascript)
      }
      js.encoding = '8bit'
      js.extensions = %w(js sj)
      js.registered = true
    end
  }
  let(:text_x_yaml) {
    mime_type('text/x-yaml') do |yaml|
      yaml.extensions = %w(yaml yml)
      yaml.encoding   = '8bit'
      yaml.friendly('en' => 'YAML Structured Document')
    end
  }
  let(:text_x_yaml_with_docs) {
    text_x_yaml.dup.tap do |yaml|
      yaml.docs = 'Test YAML'
    end
  }

  describe '.simplified' do
    it 'leaves normal types alone' do
      assert_equal 'text/plain', MIME::Type.simplified('text/plain')
    end

    it 'does not remove x- prefixes by default' do
      assert_equal 'application/x-msword',
                   MIME::Type.simplified('application/x-msword')
      assert_equal 'x-xyz/abc', MIME::Type.simplified('x-xyz/abc')
    end

    it 'removes x- prefixes when requested' do
      assert_equal 'application/msword',
                   MIME::Type.simplified('application/x-msword', remove_x_prefix: true)
      assert_equal 'xyz/abc',
                   MIME::Type.simplified('x-xyz/abc', remove_x_prefix: true)
    end

    it 'lowercases mixed-case types' do
      assert_equal 'text/vcard', MIME::Type.simplified('text/vCard')
    end

    it 'returns nil when the value provided is not a valid content type' do
      assert_nil MIME::Type.simplified('text')
    end
  end

  describe '.i18n_key' do
    it 'converts text/plain to text.plain' do
      assert_equal 'text.plain', MIME::Type.i18n_key('text/plain')
    end

    it 'does not remove x-prefixes' do
      assert_equal 'application.x-msword',
                   MIME::Type.i18n_key('application/x-msword')
    end

    it 'converts text/vCard to text.vcard' do
      assert_equal 'text.vcard', MIME::Type.i18n_key('text/vCard')
    end

    it 'returns nil when the value provided is not a valid content type' do
      assert_nil MIME::Type.i18n_key('text')
    end
  end

  describe '.new' do
    it 'fails if an invalid content type is provided' do
      exception = assert_raises MIME::Type::InvalidContentType do
        MIME::Type.new('apps')
      end
      assert_equal 'Invalid Content-Type "apps"', exception.to_s
    end

    it 'creates a valid content type just from a string' do
      type = MIME::Type.new('text/x-yaml')

      assert_instance_of MIME::Type, type
      assert_equal 'text/x-yaml', type.content_type
    end

    it 'yields the content type in a block' do
      MIME::Type.new('text/x-yaml') do |type|
        assert_instance_of MIME::Type, type
        assert_equal 'text/x-yaml', type.content_type
      end
    end

    it 'creates a valid content type from a hash' do
      type = MIME::Type.new(
        'content-type' => 'text/x-yaml',
        'obsolete' => true
      )
      assert_instance_of MIME::Type, type
      assert_equal 'text/x-yaml', type.content_type
      assert type.obsolete?
    end

    it 'creates a valid content type from an array' do
      type = MIME::Type.new(%w(text/x-yaml yaml yml yz))
      assert_instance_of MIME::Type, type
      assert_equal 'text/x-yaml', type.content_type
      assert_equal %w(yaml yml yz), type.extensions
    end
  end

  describe '#like?' do
    it 'compares two MIME::Types on #simplified values without x- prefixes' do
      assert text_plain.like?(text_plain)
      refute text_plain.like?(text_html)
    end

    it 'compares MIME::Type against string without x- prefixes' do
      assert text_plain.like?(text_plain.to_s)
      refute text_plain.like?(text_html.to_s)
    end
  end

  describe '#<=>' do
    it 'correctly compares identical types' do
      assert_equal text_plain, text_plain
    end

    it 'correctly compares equivalent types' do
      right = mime_type('text/Plain')
      refute_same text_plain, right
      assert_equal text_plain, right
    end

    it 'correctly compares types that sort earlier' do
      refute_equal text_html, text_plain
      assert_operator text_html, :<, text_plain
    end

    it 'correctly compares types that sort later' do
      refute_equal text_plain, text_html
      assert_operator text_plain, :>, text_html
    end

    it 'correctly compares types against equivalent strings' do
      assert_equal text_plain, 'text/plain'
    end

    it 'correctly compares types against strings that sort earlier' do
      refute_equal text_html, 'text/plain'
      assert_operator text_html, :<, 'text/plain'
    end

    it 'correctly compares types against strings that sort later' do
      refute_equal text_plain, 'text/html'
      assert_operator text_plain, :>, 'text/html'
    end

    it 'correctly compares against nil' do
      refute_equal text_html, nil
      assert_operator text_plain, :<, nil
    end
  end

  describe '#ascii?' do
    it 'defaults to true for text/* types' do
      assert text_plain.ascii?
    end

    it 'defaults to false for non-text/* types' do
      refute image_jpeg.ascii?
    end
  end

  describe '#binary?' do
    it 'defaults to false for text/* types' do
      refute text_plain.binary?
    end

    it 'defaults to true for non-text/* types' do
      assert image_jpeg.binary?
    end
  end

  describe '#complete?' do
    it 'is true when there are extensions' do
      assert text_x_yaml.complete?
    end

    it 'is false when there are no extensions' do
      refute mime_type('text/plain').complete?
    end
  end

  describe '#content_type' do
    it 'preserves the original case' do
      assert_equal 'text/plain', text_plain.content_type
      assert_equal 'text/vCard', mime_type('text/vCard').content_type
    end

    it 'does not remove x- prefixes' do
      assert_equal 'x-appl/x-zip', x_appl_x_zip.content_type
    end
  end

  describe '#default_encoding' do
    it 'is quoted-printable for text/* types' do
      assert_equal 'quoted-printable', text_plain.default_encoding
    end

    it 'is base64 for non-text/* types' do
      assert_equal 'base64', image_jpeg.default_encoding
    end
  end

  describe '#encoding, #encoding=' do
    it 'returns #default_encoding if not set explicitly' do
      assert_equal 'quoted-printable', text_plain.encoding
      assert_equal 'base64', image_jpeg.encoding
    end

    it 'returns the set value when set' do
      text_plain.encoding = '8bit'
      assert_equal '8bit', text_plain.encoding
    end

    it 'resets to the default encoding when set to nil or :default' do
      text_plain.encoding = '8bit'
      text_plain.encoding = nil
      assert_equal text_plain.default_encoding, text_plain.encoding
      text_plain.encoding = :default
      assert_equal text_plain.default_encoding, text_plain.encoding
    end

    it 'raises a MIME::Type::InvalidEncoding for an invalid encoding' do
      exception = assert_raises MIME::Type::InvalidEncoding do
        text_plain.encoding = 'binary'
      end
      assert_equal 'Invalid Encoding "binary"', exception.to_s
    end
  end

  describe '#eql?' do
    it 'is not true for a non-MIME::Type' do
      refute text_plain.eql?('text/plain')
    end

    it 'is not true for a different MIME::Type' do
      refute text_plain.eql?(image_jpeg)
    end

    it 'is true for an equivalent MIME::Type' do
      assert text_plain, mime_type('text/Plain')
    end
  end

  describe '#extensions, #extensions=' do
    it 'returns an array of extensions' do
      assert_equal %w(yaml yml), text_x_yaml.extensions
      assert_equal %w(zip zp), x_appl_x_zip.extensions
    end

    it 'sets a single extension when provided a single value' do
      text_x_yaml.extensions = 'yaml'
      assert_equal %w(yaml), text_x_yaml.extensions
    end

    it 'deduplicates extensions' do
      text_x_yaml.extensions = %w(yaml yaml)
      assert_equal %w(yaml), text_x_yaml.extensions
    end
  end

  describe '#add_extensions' do
    it 'does not modify extensions when provided nil' do
      text_x_yaml.add_extensions(nil)
      assert_equal %w(yaml yml), text_x_yaml.extensions
    end

    it 'remains deduplicated with duplicate values' do
      text_x_yaml.add_extensions('yaml')
      assert_equal %w(yaml yml), text_x_yaml.extensions
      text_x_yaml.add_extensions(%w(yaml yz))
      assert_equal %w(yaml yml yz), text_x_yaml.extensions
    end
  end

  describe '#priority_compare' do
    def assert_priority_less(left, right)
      assert_equal(-1, left.priority_compare(right))
    end

    def assert_priority_same(left, right)
      assert_equal 0, left.priority_compare(right)
    end

    def assert_priority_more(left, right)
      assert_equal 1, left.priority_compare(right)
    end

    def assert_priority(left, middle, right)
      assert_priority_less left, right
      assert_priority_same left, middle
      assert_priority_more right, left
    end

    let(:text_1) { mime_type('text/1') }
    let(:text_1p) { mime_type('text/1') }
    let(:text_2) { mime_type('text/2') }

    it 'sorts (1) based on the simplified type' do
      assert_priority text_1, text_1p, text_2
    end

    it 'sorts (2) based on extensions' do
      text_1.extensions = ['foo', 'bar']
      text_2.extensions = ['foo']

      assert_priority_same text_1, text_2

      text_2.registered = true

      assert_priority_more text_1, text_2
    end

    it 'sorts (3) based on the registration state' do
      text_1.registered = text_1p.registered = true
      text_1b = mime_type(text_1) { |t| t.registered = false }

      assert_priority text_1, text_1p, text_1b
    end

    it 'sorts (4) based on the completeness' do
      text_1.extensions = text_1p.extensions = '1'
      text_1b = mime_type(text_1) { |t| t.extensions = nil }

      assert_priority text_1, text_1p, text_1b
    end

    it 'sorts (5) based on obsolete status' do
      text_1.obsolete = text_1p.obsolete = false
      text_1b = mime_type(text_1) { |t| t.obsolete = true }

      assert_priority text_1, text_1p, text_1b
    end

    it 'sorts (5) based on the use-instead value' do
      text_1.obsolete = text_1p.obsolete = true
      text_1.use_instead = text_1p.use_instead = 'abc/xyz'
      text_1b = mime_type(text_1) { |t| t.use_instead = nil }

      assert_priority text_1, text_1p, text_1b

      text_1b.use_instead = 'abc/zzz'

      assert_priority text_1, text_1p, text_1b
    end
  end

  describe '#raw_media_type' do
    it 'extracts the media type as case-preserved' do
      assert_equal 'Text', mime_type('Text/plain').raw_media_type
    end

    it 'does not remove x- prefixes' do
      assert_equal('x-appl', x_appl_x_zip.raw_media_type)
    end
  end

  describe '#media_type' do
    it 'extracts the media type as lowercase' do
      assert_equal 'text', text_plain.media_type
    end

    it 'does not remove x- prefixes' do
      assert_equal('x-appl', x_appl_x_zip.media_type)
    end
  end

  describe '#raw_media_type' do
    it 'extracts the media type as case-preserved' do
      assert_equal 'Text', mime_type('Text/plain').raw_media_type
    end

    it 'does not remove x- prefixes' do
      assert_equal('x-appl', x_appl_x_zip.raw_media_type)
    end
  end

  describe '#sub_type' do
    it 'extracts the sub type as lowercase' do
      assert_equal 'plain', text_plain.sub_type
    end

    it 'does not remove x- prefixes' do
      assert_equal('x-zip', x_appl_x_zip.sub_type)
    end
  end

  describe '#raw_sub_type' do
    it 'extracts the sub type as case-preserved' do
      assert_equal 'Plain', mime_type('text/Plain').raw_sub_type
    end

    it 'does not remove x- prefixes' do
      assert_equal('x-zip', x_appl_x_zip.raw_sub_type)
    end
  end

  describe '#to_h' do
    let(:t) { mime_type('a/b') }

    it 'has the required keys (content-type, registered, encoding)' do
      assert_has_keys t.to_h, %w(content-type registered encoding)
    end

    it 'has the docs key if there are documents' do
      assert_has_keys mime_type(t) { |v| v.docs = 'a' }.to_h, %w(docs)
    end

    it 'has the extensions key if set' do
      assert_has_keys mime_type(t) { |v| v.extensions = 'a' }.to_h,
                      'extensions'
    end

    it 'has the preferred-extension key if set' do
      assert_has_keys mime_type(t) { |v| v.preferred_extension = 'a' }.to_h,
                      'preferred-extension'
    end

    it 'has the obsolete key if set' do
      assert_has_keys mime_type(t) { |v| v.obsolete = true }.to_h, 'obsolete'
    end

    it 'has the obsolete and use-instead keys if set' do
      assert_has_keys mime_type(t) { |v|
        v.obsolete = true
        v.use_instead = 'c/d'
      }.to_h, %w(obsolete use-instead)
    end

    it 'has the signature key if set' do
      assert_has_keys mime_type(t) { |v| v.signature = true }.to_h, 'signature'
    end
  end

  describe '#to_json' do
    let(:expected) {
      '{"content-type":"a/b","encoding":"base64","registered":false}'
    }

    it 'converts to JSON when requested' do
      assert_equal expected, mime_type('a/b').to_json
    end
  end

  describe '#to_s, #to_str' do
    it 'represents itself as a string of the canonical content_type' do
      assert_equal 'text/plain', text_plain.to_s
    end

    it 'acts like a string of the canonical content_type for comparison' do
      assert_equal text_plain, 'text/plain'
    end

    it 'acts like a string for other purposes' do
      assert_equal 'stringy', 'text/plain'.sub(text_plain, 'stringy')
    end
  end

  describe '#xrefs, #xrefs=' do
    let(:expected) {
      MIME::Types::Container.new('rfc' => Set['rfc1234', 'rfc5678'])
    }

    it 'returns the expected results' do
      application_javascript.xrefs = {
        'rfc' => %w(rfc5678 rfc1234 rfc1234)
      }

      assert_equal expected, application_javascript.xrefs
    end
  end

  describe '#xref_urls' do
    let(:expected) {
      [
        'http://www.iana.org/go/draft1',
        'http://www.iana.org/assignments/media-types/a/b',
        'http://www.iana.org/assignments/media-types/media-types.xhtml#p-1',
        'http://www.iana.org/go/rfc-1',
        'http://www.rfc-editor.org/errata_search.php?eid=err-1',
        'http://example.org',
        'text'
      ]
    }

    let(:type) {
      mime_type('a/b').tap do |t|
        t.xrefs = {
          'draft' => ['RFC1'],
          'template' => ['a/b'],
          'person' => ['p-1'],
          'rfc' => ['rfc-1'],
          'rfc-errata' => ['err-1'],
          'uri' => ['http://example.org'],
          'text' => ['text']
        }
      end
    }

    it 'translates according to given rules' do
      assert_equal expected, type.xref_urls
    end
  end

  describe '#use_instead' do
    it 'is nil unless the type is obsolete' do
      assert_nil text_plain.use_instead
    end

    it 'is nil if not set and the type is obsolete' do
      text_plain.obsolete = true
      assert_nil text_plain.use_instead
    end

    it 'is a different type if set and the type is obsolete' do
      text_plain.obsolete = true
      text_plain.use_instead = 'text/html'
      assert_equal 'text/html', text_plain.use_instead
    end
  end

  describe '#preferred_extension, #preferred_extension=' do
    it 'is nil when not set and there are no extensions' do
      assert_nil text_plain.preferred_extension
    end

    it 'is the first extension when not set but there are extensions' do
      assert_equal 'yaml', text_x_yaml.preferred_extension
    end

    it 'is the extension provided when set' do
      text_x_yaml.preferred_extension = 'yml'
      assert_equal 'yml', text_x_yaml.preferred_extension
    end

    it 'is adds the preferred extension if it does not exist' do
      text_x_yaml.preferred_extension = 'yz'
      assert_equal 'yz', text_x_yaml.preferred_extension
      assert_includes text_x_yaml.extensions, 'yz'
    end
  end

  describe '#friendly' do
    it 'returns English by default' do
      assert_equal 'YAML Structured Document', text_x_yaml.friendly
    end

    it 'returns English when requested' do
      assert_equal 'YAML Structured Document', text_x_yaml.friendly('en')
      assert_equal 'YAML Structured Document', text_x_yaml.friendly(:en)
    end

    it 'returns nothing for an unknown language' do
      assert_nil text_x_yaml.friendly('zz')
    end

    it 'merges new values from an array parameter' do
      expected = { 'en' => 'Text files' }
      assert_equal expected, text_plain.friendly(['en', 'Text files'])
      expected.update('fr' => 'des fichiers texte')
      assert_equal expected,
                   text_plain.friendly(['fr', 'des fichiers texte'])
    end

    it 'merges new values from a hash parameter' do
      expected = { 'en' => 'Text files' }
      assert_equal expected, text_plain.friendly(expected)
      french = { 'fr' => 'des fichiers texte' }
      expected.update(french)
      assert_equal expected, text_plain.friendly(french)
    end

    it 'raises an ArgumentError if an unknown value is provided' do
      exception = assert_raises ArgumentError do
        text_plain.friendly(1)
      end

      assert_equal 'Expected a language or translation set, not 1',
                   exception.message
    end
  end
end
