require 'test_helper'

class TagsControllerTest < ActionController::TestCase
  include Devise::Test::ControllerHelpers

  test 'index with json format should return JSON list of tags' do
    get :index, params: { format: 'json' }

    assert_response(:success)
    assert_valid_json_response
    assert_not_nil assigns(:tags)
  end

  test 'index with search params should return tags including search term' do
    get :index, params: { format: 'json', term: 'dis' }

    assert_response(:success)
    assert_valid_json_response
    assert_not_nil assigns(:tags)

    JSON.parse(response.body).each do |tag|
      assert_equal true, tag['name'].include?('dis') || tag['tag_synonyms'].any? { |ts| ts['name'].include?('syn') }
    end
  end

  test 'index with search params should return tags whose synonyms include search term' do
    get :index, params: { format: 'json', term: 'syn' }

    assert_response(:success)
    assert_valid_json_response
    assert_not_nil assigns(:tags)

    JSON.parse(response.body).each do |tag|
      assert_equal true, tag['name'].include?('syn') || tag['tag_synonyms'].any? { |ts| ts['name'].include?('syn') }
    end
  end

  test 'should get category tags list' do
    get :category, params: { id: categories(:main).id }

    assert_response(:success)
    assert_not_nil assigns(:tags)
    assert_not_nil assigns(:category)

    sign_in users(:standard_user)
    get :category, params: { id: categories(:main).id }

    assert_response(:success)
    assert_not_nil assigns(:tags)
    assert_not_nil assigns(:category)
  end

  test 'should get children list' do
    get :children, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_response(:success)
    assert_not_nil assigns(:tags)
    assert_not_nil assigns(:category)

    sign_in users(:standard_user)
    get :children, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_response(:success)
    assert_not_nil assigns(:tags)
    assert_not_nil assigns(:category)
  end

  test 'should get tag page and RSS' do
    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_response(:success)
    assert_not_nil assigns(:tag)
    assert_not_nil assigns(:category)
    assert_not_nil assigns(:posts)

    sign_in users(:standard_user)
    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_response(:success)
    assert_not_nil assigns(:tag)
    assert_not_nil assigns(:category)
    assert_not_nil assigns(:posts)
  end

  test 'should get tag RSS feed' do
    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id, format: :rss }

    assert_response(:success)
    assert_not_nil assigns(:tag)
    assert_not_nil assigns(:category)
    assert_not_nil assigns(:posts)

    sign_in users(:standard_user)
    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id, format: :rss }

    assert_response(:success)
    assert_not_nil assigns(:tag)
    assert_not_nil assigns(:category)
    assert_not_nil assigns(:posts)
  end

  test ':create should require authentication' do
    try_create_tag(categories(:main), tag_sets(:main))

    assert_redirected_to_sign_in
  end

  test 'only users who can edit tags should be able to create them' do
    category = categories(:main)
    tag_set = tag_sets(:main)

    users.each do |user|
      sign_in(user)

      draft_path = create_tag_path(id: category.id)
      tag_name = "test-tag-#{user.id}"

      @controller.stub('params', { tag_name: tag_name }) do
        @controller.do_save_draft(user, draft_path)
      end

      try_create_tag(category, tag_set, name: tag_name)

      if !@controller.helpers.user_signed_in?
        assert_redirected_to_sign_in
      elsif user.can_edit_tags?
        assert_response(:found, "Expected user '#{user.name}' to be able to create tags")
        assert_draft_deleted(user, draft_path, :tag_name)
      else
        assert_response(:not_found, "Expected user '#{user.name}' to not be able to create tags")
      end
    end
  end

  test ':create should correctly create tags' do
    sign_in users(:tags_editor)

    category = categories(:main)
    tag_set = tag_sets(:main)
    tag_name = 'test-tag'

    try_create_tag(category, tag_set, name: tag_name)

    @tag = assigns(:tag)

    assert_not_nil @tag
    assert_redirected_to(tag_path(id: category.id, tag_id: @tag.id))
  end

  test 'should deny edit to anonymous user' do
    get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_redirected_to_sign_in
  end

  test 'should deny edit to unprivileged user' do
    sign_in users(:standard_user)
    get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_response(:forbidden)
  end

  test 'should get edit' do
    sign_in users(:deleter)
    get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }

    assert_response(:success)
    assert_not_nil assigns(:tag)
    assert_not_nil assigns(:category)
  end

  test 'should deny update to anonymous user' do
    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
                             tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }

    assert_redirected_to_sign_in
  end

  test 'should deny update to unprivileged user' do
    sign_in users(:standard_user)
    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
                             tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }

    assert_response(:forbidden)
  end

  test 'should update tag' do
    sign_in users(:deleter)
    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
                             tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }

    assert_response(:found)
    assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:topic).id)
    assert_not_nil assigns(:tag)
    assert_equal tags(:discussion).id, assigns(:tag).parent_id
    assert_equal 'things', assigns(:tag).excerpt
  end

  test 'should update tag with synonym addition' do
    sign_in users(:deleter)
    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
                             tag: { tag_synonyms_attributes: { '1': { name: 'conversation' } } } }

    assert_response(:found)
    assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:topic).id)
    assert_not_nil assigns(:tag)
    assert_equal 'conversation', assigns(:tag).tag_synonyms.first&.name
  end

  test 'should update tag with synonym removal' do
    sign_in users(:deleter)
    patch :update, params: { id: categories(:main).id, tag_id: tags(:base).id,
                             tag: { tag_synonyms_attributes: { '1': { id: tag_synonyms(:base_synonym).id, _destroy: 'true' } } } }

    assert_response(:found)
    assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:base).id)
    assert_not_nil assigns(:tag)
    assert_equal true, (assigns(:tag).tag_synonyms.none? { |ts| ts.name == 'synonym' })
  end

  test 'should prevent a tag being its own parent' do
    sign_in users(:deleter)
    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
                             tag: { parent_id: tags(:topic).id, excerpt: 'things' } }

    assert_response(:bad_request)
    assert_not_nil assigns(:tag)
    assert_equal ['A tag cannot be its own parent.'], assigns(:tag).errors.full_messages
  end

  test 'should prevent hierarchical loops' do
    sign_in users(:deleter)
    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
                             tag: { parent_id: tags(:child).id, excerpt: 'things' } }

    assert_response(:bad_request)
    assert_not_nil assigns(:tag)
    assert_equal ["The #{tags(:child).name} tag is already a child of this tag."], assigns(:tag).errors.full_messages
  end

  test 'should correctly rename a tag' do
    sign_in users(:moderator)

    tag = tags(:base)
    new_tag_name = 'renamed'

    try_rename_tag(categories(:main), tag, new_tag_name)

    assert_json_success
    res_body = JSON.parse(response.body)
    assert_equal new_tag_name, res_body['tag']['name']

    log_entry = AuditLog.last
    assert_equal 'tag_rename', log_entry['event_type']
    assert_equal tag.id, log_entry['related_id']
  end

  test 'should prevent renaming a tag to an invalid name' do
    sign_in users(:moderator)

    tag = tags(:base)
    old_name = tag.name

    {
      '' => 'no_blank_name',
      'name with spaces' => 'no_spaces_in_name',
      tags(:discussion).name => 'no_duplicate_names'
    }.each do |new_name, error_key|
      try_rename_tag(categories(:main), tag, new_name)

      assert_json_failure(:bad_request)
      res_body = JSON.parse(response.body)
      error_message = I18n.t("tags.validation.errors.#{error_key}")

      assert res_body['errors'].any? { |msg| msg.include?(error_message) },
             "Expected '#{error_message}' to be among the errors"

      tag.reload
      assert_equal tag.name, old_name
    end
  end

  private

  # @param category [Category] category to create the tag in
  # @param tag_set [TagSet] tag set the tag belongs to
  def try_create_tag(tag_set, category, **opts)
    # this is needed for draft deletion until we switch from request.referer
    @request.set_header('HTTP_REFERER', create_tag_path(id: category.id))

    post :create, params: {
      id: category.id,
      tag: {
        excerpt: 'Usage guidance goes here',
        name: 'test-tag',
        tag_set_id: tag_set.id,
        wiki_markdown: 'Extended tag description goes here'
      }.merge(opts)
    }
  end

  # @param category [Category] category to rename the tag in
  # @param tag [Tag] tag to rename
  # @param name [String] new tag name
  def try_rename_tag(category, tag, name)
    post :rename, params: {
      format: :json,
      id: category.id,
      name: name,
      tag_id: tag.id,
      tag: tag
    }
  end
end
