# encoding: UTF-8

# Copyright 2011-2013 innoQ Deutschland GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require File.join(File.expand_path(File.dirname(__FILE__)), '../test_helper')

class HierarchyTest < ActionController::TestCase
  setup do
    @controller = HierarchyController.new

    # create a concept hierarchy
    concepts = YAML.load <<-EOS
root:
  foo:
  bar:
    alpha:
    bravo:
      uno:
      dos:
        lorem:
        ipsum:
    EOS
    @rel_class = Iqvoc::Concept.broader_relation_class.narrower_class
    @concepts = create_hierarchy(concepts, @rel_class, {})
    @concepts['root'].update_attribute('top_term', true)
  end

  test 'entire hierarchy' do
    additional_concepts = YAML.load <<-EOS
boot:
  zoo:
  car:
    EOS
    @concepts.merge! create_hierarchy(additional_concepts, @rel_class, {})
    @concepts['boot'].update_attribute('top_term', true)

    get :index, params: { lang: 'en', format: 'ttl' }
    assert_response 200
    %w(root foo bar alpha bravo uno dos boot zoo car).each do |id|
      assert @response.body.include?(":#{id} a skos:Concept;"), "#{id} missing"
    end
    %w(lorem ipsum).each do |id|
      assert (not @response.body.include?(":#{id} a skos:Concept;")),
          "#{id} should not be present"
    end

    Iqvoc.config['performance.unbounded_hierarchy'] = true
    get :index, params: { lang: 'en', format: 'ttl' }
    assert_response 200

    %w(root foo bar alpha bravo uno dos lorem ipsum boot zoo car).each do |id|
      assert @response.body.include?(":#{id} a skos:Concept;"), "#{id} missing"
    end
  end

  test 'permission handling' do
    get :show, params: { lang: 'en', format: 'html', root: 'root' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Root'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li')
    assert_equal ['Bar', 'Foo'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li > ul > li')
    assert_equal ['Alpha', 'Bravo'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li > ul > li > ul > li')
    assert_equal ['Dos', 'Uno'], entries
    entries = css_select('ul.concept-hierarchy > li > ul > li > ul > li > ul > li > ul > li')
    assert_equal 0, entries.length # exceeded default depth

    @concepts['bar'].update_attribute('published_at', nil)

    get :show, params: { lang: 'en', format: 'html', root: 'root' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Root'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li')
    assert_equal ['Foo'], entries
    entries = get_entries('ul.concept-hierarchy li > ul > li > ul > li')
    assert_equal 0, entries.length
  end

  test 'caching' do
    # ETag generation & cache control
    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    etag = @response.headers['ETag']
    assert_response 200
    assert etag
    assert @response.headers['Cache-Control'].include?('public')

    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    assert_response 200
    assert_equal etag, @response.headers['ETag']

    get :show, params: { lang: 'en', format: 'ttl', root: 'root', published: 0 }
    assert_response 200
    assert @response.headers['Cache-Control'].include?('private')

    # ETag keyed on params

    get :show, params: { lang: 'en', format: 'ttl', root: 'root' , depth: 4 }
    assert_response 200
    assert_not_equal etag, @response.headers['ETag']

    get :show, params: { lang: 'en', format: 'ttl', root: 'root', published: 0 }
    assert_response 200
    assert_not_equal etag, @response.headers['ETag']

    # ETag keyed on (any in-scope) concept modification

    t0 = Time.now
    t1 = Time.now + 30

    dummy = create_concept('dummy', 'Dummy', 'en', false)
    dummy.update_attribute('updated_at', t1)
    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    assert_response 200
    assert_equal etag, @response.headers['ETag']

    dummy.update_attribute('published_at', t0)
    dummy.update_attribute('updated_at', t1)
    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    assert_response 200
    new_etag = @response.headers['ETag']
    assert_not_equal etag, new_etag

    # conditional caching

    @request.env['HTTP_IF_NONE_MATCH'] = new_etag
    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    assert_response 304
    assert_equal 0, @response.body.strip.length

    @request.env['HTTP_IF_NONE_MATCH'] = 'dummy'
    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    assert_response 200
  end

  test 'RDF representations' do
    # Turtle

    get :show, params: { lang: 'en', format: 'ttl', root: 'root' }
    assert_response 200
    assert_equal @response.media_type, 'text/turtle'
    assert @response.body =~ /:root[^\.]+skos:topConceptOf[^\.]+:scheme/m
    assert @response.body =~ /:root[^\.]+skos:prefLabel[^\.]+"Root"@en/m
    assert @response.body =~ /:root[^\.]+skos:narrower[^\.]+:bar/m
    assert @response.body =~ /:root[^\.]+skos:narrower[^\.]+:foo/m
    assert @response.body.include?(<<-EOS)
:foo a skos:Concept;
     skos:prefLabel "Foo"@en.
    EOS
    assert @response.body =~ /:bar[^\.]+skos:prefLabel[^\.]+"Bar"@en/m
    assert @response.body =~ /:bar[^\.]+skos:narrower[^\.]+:alpha/m
    assert @response.body =~ /:bar[^\.]+skos:narrower[^\.]+:bravo/m
    assert @response.body.include?(<<-EOS)
:alpha a skos:Concept;
       skos:prefLabel "Alpha"@en.
    EOS
    assert @response.body =~ /:bravo[^\.]+skos:prefLabel[^\.]+"Bravo"@en/m
    assert @response.body =~ /:bravo[^\.]+skos:narrower[^\.]+:uno/m
    assert @response.body =~ /:bravo[^\.]+skos:narrower[^\.]+:dos/m
    assert @response.body.include?(<<-EOS)
:uno a skos:Concept;
     skos:prefLabel "Uno"@en.
    EOS
    assert @response.body.include?(<<-EOS)
:dos a skos:Concept;
     skos:prefLabel "Dos"@en.
    EOS

    get :show, params: { lang: 'en', format: 'ttl', root: 'lorem', dir: 'up' }
    assert_response 200
    assert_equal @response.media_type, 'text/turtle'
    assert @response.body.include?(<<-EOS)
:lorem a skos:Concept;
       skos:prefLabel "Lorem"@en;
       skos:broader :dos.
    EOS
    assert @response.body.include?(<<-EOS)
:lorem a skos:Concept;
       skos:prefLabel "Lorem"@en;
       skos:broader :dos.
    EOS
    assert @response.body.include?(<<-EOS)
:dos a skos:Concept;
     skos:prefLabel "Dos"@en;
     skos:broader :bravo.
    EOS
    assert @response.body.include?(<<-EOS)
:bravo a skos:Concept;
       skos:prefLabel "Bravo"@en;
       skos:broader :bar.
    EOS
    assert @response.body.include?(<<-EOS)
:bar a skos:Concept;
     skos:prefLabel "Bar"@en.
    EOS

    # RDF/XML

    get :show, params: { lang: 'en', format: 'rdf', root: 'root' }
    assert_response 200
    assert_equal @response.media_type, 'application/rdf+xml'
  end

  test 'root parameter handling' do
    assert_raises ActionController::UrlGenerationError do
      get :show, params: { format: 'html' }
    end

    get :show, params: { lang: 'en', format: 'html', root: 'N/A' }
    assert_response 404
    assert_equal 'no concept matching root parameter', flash[:error]
    entries = css_select('ul.concept-hierarchy li')
    assert_equal 0, entries.length

    get :show, params: { lang: 'en', format: 'html', root: 'root' }
    assert_response 200
    assert_nil flash[:error]
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal 1, entries.length
    assert_equal 'Root', entries[0]

    get :show, params: { lang: 'en', format: 'html', root: 'root' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Root'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li')
    assert_equal ['Bar', 'Foo'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li > ul > li')
    assert_equal ['Alpha', 'Bravo'], entries
    entries = get_entries('ul.concept-hierarchy li > ul > li > ul > li > ul > li')
    assert_equal ['Dos', 'Uno'], entries
    entries = css_select('ul.concept-hierarchy > li > ul > li > ul > li > ul > li > ul > li')
    assert_equal 0, entries.length # exceeded default depth

    get :show, params: { lang: 'en', format: 'html', root: 'bravo' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Bravo'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li')
    assert_equal ['Dos', 'Uno'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li > ul > li')
    assert_equal ['Ipsum', 'Lorem'], entries
    entries = css_select('ul.concept-hierarchy > li > ul > li > ul > li > ul > li')
    assert_equal 0, entries.length

    get :show, params: { lang: 'en', format: 'html', root: 'lorem' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Lorem'], entries
    entries = css_select('ul.concept-hierarchy > li > ul > li')
    assert_equal 0, entries.length
  end

  test 'depth handling' do
    selector = 'ul.concept-hierarchy > li > ul > li > ul > li > ul > li > ul > li'

    get :show, params: { lang: 'en', format: 'html', root: 'root' }
    entries = css_select(selector)
    assert_equal 0, entries.length # default depth is 3

    get :show, params: { lang: 'en', format: 'html', root: 'root', depth: 4 }
    entries = css_select(selector)
    assert_equal 2, entries.length

    get :show, params: { lang: 'en', format: 'html', root: 'root', depth: 1 }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Root'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li')
    assert_equal ['Bar', 'Foo'], entries
    entries = css_select('ul.concept-hierarchy > li > ul > li > ul > li')
    assert_equal 0, entries.length

    old_config_value = Iqvoc.config['performance.unbounded_hierarchy']
    Iqvoc.config['performance.unbounded_hierarchy'] = false
    get :show, params: { lang: 'en', format: 'html', root: 'root', depth: 5 }
    assert_response 403
    assert_equal 'excessive depth', flash[:error]
    Iqvoc.config['performance.unbounded_hierarchy'] = old_config_value

    get :show, params: { lang: 'en', format: 'html', root: 'root', depth: 'invalid' }
    assert_response 400
    assert_equal 'invalid depth parameter', flash[:error]
  end

  test 'direction handling' do
    get :show, params: { lang: 'en', format: 'html', root: 'root' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Root'], entries
    entries = get_entries('ul.concept-hierarchy > li > ul > li > ul > li > ul > li')
    assert_equal ['Dos', 'Uno'], entries

    get :show, params: { lang: 'en', format: 'html', root: 'root', dir: 'up' }
    entries = get_entries('ul.concept-hierarchy li')
    assert_equal ['Root'], entries
    entries = css_select('ul.concept-hierarchy li li')
    assert_equal 0, entries.length

    get :show, params: {  lang: 'en', format: 'html', root: 'lorem' }
    entries = get_entries('ul.concept-hierarchy li')
    assert_equal ['Lorem'], entries
    entries = css_select('ul.concept-hierarchy li li')
    assert_equal 0, entries.length

    get :show, params: { lang: 'en', format: 'html', root: 'lorem', dir: 'up' }
    entries = get_entries('ul.concept-hierarchy > li')
    assert_equal ['Lorem'], entries
    entries = get_entries('ul.concept-hierarchy li > ul > li > ul > li > ul > li')
    assert_equal ['Bar'], entries
    entries = css_select('ul.concept-hierarchy li > ul > li > ul > li > ul > li > ul > li')
    assert_equal 0, entries.length

    get :show, params: { lang: 'en', format: 'html', root: 'lorem', dir: 'up', depth: 4 }
    page.all('ul.concept-hierarchy li').
        map { |node| node.native.children.first.text }
    entries = get_entries('ul.concept-hierarchy > li > ul > li > ul > li > ul > li > ul > li')
    assert_equal ['Root'], entries
  end

  test 'siblings handling' do
    get :show, params: { lang: 'en', format: 'html', root: 'foo' }
    entries = get_all_entries('ul.concept-hierarchy li')
    assert_equal ['Foo'], entries

    get :show, params: { lang: 'en', format: 'html', root: 'foo', siblings: 'true' }
    entries = get_all_entries('ul.concept-hierarchy li')
    assert_equal ['Bar', 'Foo'], entries

    get :show, params: { lang: 'en', format: 'html', root: 'lorem' }
    entries = get_all_entries('ul.concept-hierarchy li')
    assert_equal ['Lorem'], entries

    get :show, params: { lang: 'en', format: 'html', root: 'lorem', dir: 'up', siblings: 'true' }
    entries = get_all_entries('ul.concept-hierarchy li')
    assert_equal 8, entries.length
    ['Lorem', 'Ipsum', 'Uno', 'Dos', 'Alpha', 'Bravo', 'Bar', 'Foo'].each do |name|
      assert entries.include?(name), "missing entry: #{name}"
    end

    get :show, params: { lang: 'en', format: 'html', root: 'lorem', dir: 'up', siblings: '1', depth: 4 }
    entries = get_all_entries('ul.concept-hierarchy li')
    assert_equal 9, entries.length
    ['Lorem', 'Ipsum', 'Uno', 'Dos', 'Alpha', 'Bravo', 'Bar', 'Foo', 'Root'].each do |name|
      assert entries.include?(name), "missing entry: #{name}"
    end
  end

  test 'avoid duplication' do # in response to a bug report
    get :show, params: { lang: 'en', format: 'ttl', root: 'uno', dir: 'up' }
    assert_response 200
    assert_equal 'text/turtle', @response.media_type
    assert @response.body.include?(<<-EOS)
:bravo a skos:Concept;
       skos:prefLabel "Bravo"@en;
       skos:broader :bar.
    EOS
    assert (not @response.body.include?(':bravo skos:prefLabel "Bravo"@en.'))
  end

  def get_all_entries(selector)
    return page.all(selector).map { |node| node.native.children.first.text }
  end

  def get_entries(selector)
    return css_select(selector).map { |node| node.children.first.content }
  end

  def page # XXX: should not be necessary!?
    return Capybara::Node::Simple.new(@response.body)
  end

  def create_hierarchy(hash, rel_class, memo = nil, parent =nil)
    hash.each do |origin, children|
      concept = create_concept(origin, origin.capitalize, 'en')
      memo[origin] = concept if memo
      link_concepts(parent, rel_class, concept) if parent
      create_hierarchy(children, rel_class, memo, concept) unless children.blank?
    end
    return memo
  end

  def link_concepts(source, rel_class, target)
    rel_name = rel_class.name.to_relation_name
    source.send(rel_name).create_with_reverse_relation(target)
  end

  def create_concept(origin, pref_label, label_lang, published=true)
    concept = Iqvoc::Concept.base_class.create(origin: origin,
        published_at: (published ? Time.now : nil))
    label = Iqvoc::Label.base_class.create(value: pref_label,
        language: label_lang)
    labeling = Iqvoc::Concept.pref_labeling_class.create(owner: concept,
        target: label)
    return concept
  end
end
