#  Copyright (c) 2012-2013, Jungwacht Blauring Schweiz. This file is part of
#  hitobito and licensed under the Affero General Public License version 3
#  or later. See the COPYING file at the top-level directory or at
#  https://github.com/hitobito/hitobito.

# A dummy model used for general testing.
class CrudTestModel < ActiveRecord::Base # :nodoc:
  include DatetimeAttribute
  datetime_attr :last_seen

  belongs_to :companion, class_name: "CrudTestModel"
  has_and_belongs_to_many :others, class_name: "OtherCrudTestModel"
  has_many :mores, class_name: "OtherCrudTestModel", foreign_key: :more_id

  before_destroy :protect_if_companion

  validates :name, presence: true
  validates :birthdate, timeliness: {type: :date, allow_blank: true}
  validates :rating, inclusion: {in: 1..10}

  default_scope -> { order("name") }

  def to_s
    name
  end

  def chatty
    remarks.size
  end

  def klass
    self.class
  end

  private

  def protect_if_companion
    if companion.present?
      errors.add(:base, "Cannot destroy model with companion")
      throw :abort
    end
  end
end

class CrudTestModelDecorator < Draper::Decorator
  delegate_all
  decorates :crud_test_model
end

class OtherCrudTestModel < ActiveRecord::Base # :nodoc:
  has_and_belongs_to_many :others, class_name: "CrudTestModel"
  belongs_to :more, class_name: "CrudTestModel"

  def to_s
    name
  end
end

# Controller for the dummy model.
class CrudTestModelsController < CrudController # :nodoc:
  HANDLE_PREFIX = "handle_"

  self.search_columns = [:name, :whatever, :remarks]
  self.sort_mappings = {chatty: "length(remarks)"}
  self.permitted_attrs = [:name, :email, :password, :whatever, :children,
    :companion_id, :rating, :income, :birthdate,
    :gets_up_at, :last_seen, :human, :remarks]

  skip_authorize_resource
  skip_authorization_check

  decorates :crud_test_model, :crud_test_models

  before_create :possibly_redirect
  before_create :handle_name
  before_destroy :handle_name

  before_render_new :possibly_redirect
  before_render_new :set_companions

  attr_reader :called_callbacks
  attr_accessor :should_redirect

  # don't use the standard layout as it may require different routes
  # than just the test route for this controller
  layout false

  def index
    super do |format|
      format.js { render plain: "index js" }
    end
  end

  def show
    super do |format|
      format.html { render plain: "custom html" } if entry.name == "BBBBB"
    end
  end

  def create
    super do |format|
      flash[:notice] = "model got created" if entry.persisted?
    end
  end

  private

  def list_entries
    entries = super
    if params[:filter]
      entries = entries.where(rating: ...3).except(:order).order("children DESC")
    end
    entries
  end

  private

  # custom callback
  def handle_name
    if entry.name == "illegal"
      flash[:alert] = "illegal name"
      throw(:abort)
    end
  end

  # create callback methods that record the before/after callbacks
  [:create, :update, :save, :destroy].each do |a|
    callback = "before_#{a}"
    send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}")
    callback = "after_#{a}"
    send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}")
  end

  # create callback methods that record the before_render callbacks
  [:index, :show, :new, :edit, :form].each do |a|
    callback = "before_render_#{a}"
    send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}")
  end

  # handle the called callbacks
  def method_missing(sym, *args)
    called_callback(sym.to_s[HANDLE_PREFIX.size..].to_sym) if sym.to_s.starts_with?(HANDLE_PREFIX)
  end

  # callback to redirect if @should_redirect is set
  def possibly_redirect
    redirect_to action: "index" if should_redirect && !performed?
    throw :abort if should_redirect
  end

  def set_companions
    @companions = CrudTestModel.where(human: true)
  end

  # records a callback
  def called_callback(callback)
    @called_callbacks ||= []
    @called_callbacks << callback
  end

  def authorize_class
    # nada
  end
end

REGEXP_ROWS = /<tr.+?<\/tr>/m
REGEXP_HEADERS = /<th.+?<\/th>/m
REGEXP_SORT_HEADERS = /<th.*?><a .*?sort_dir=asc.*?>.*?<\/a><\/th>/m
REGEXP_ACTION_CELL = /<td class="action"><a .*?href.+?<\/a><\/td>/m

# A simple test helper to prepare the test database with a CrudTestModel model.
# This helper is used to test the CrudController and various helpers
# without the need for an application based model.
module CrudTestHelper
  # Controller helper methods for the tests

  def model_class
    CrudTestModel
  end

  def controller_name
    "crud_test_models"
  end

  def action_name
    "index"
  end

  def params
    {}
  end

  def path_args(entry)
    entry
  end

  def sortable?(attr)
    true
  end

  def h(text)
    ERB::Util.h(text)
  end

  private

  # Sets up the test database with a crud_test_models table.
  # Look at the source to view the column definition.
  def setup_db
    without_transaction do
      silence_warnings do
        create_crud_test_models_table
        create_other_crud_test_models_table
        create_crud_test_models_other_crud_test_models
      end
      CrudTestModel.reset_column_information
    end
  end

  def create_crud_test_models_table
    ActiveRecord::Base.connection.create_table :crud_test_models, force: true do |t|
      t.string :name, null: false, limit: 50
      t.string :password
      t.string :whatever
      t.integer :children
      t.integer :companion_id
      t.float :rating
      t.decimal :income, precision: 14, scale: 2
      t.date :birthdate
      t.time :gets_up_at
      t.datetime :last_seen
      t.boolean :human, default: true
      t.text :remarks

      t.timestamps null: false
    end
  end

  def create_other_crud_test_models_table
    ActiveRecord::Base.connection.create_table :other_crud_test_models, force: true do |t|
      t.string :name, null: false, limit: 50
      t.integer :more_id
    end
  end

  def create_crud_test_models_other_crud_test_models
    ActiveRecord::Base.connection.create_table :crud_test_models_other_crud_test_models, force: true do |t|
      t.belongs_to :crud_test_model, index: {name: "one"}
      t.belongs_to :other_crud_test_model, index: {name: "other"}
    end
  end

  # Removes the crud_test_models table from the database.
  def reset_db
    c = ActiveRecord::Base.connection
    [:crud_test_models, :other_crud_test_models, :crud_test_models_other_crud_test_models].each do |table|
      if c.data_source_exists?(table)
        begin
          c.drop_table(table)
        rescue
          nil
        end
      end
    end
  end

  # Creates 6 dummy entries for the crud_test_models table.
  def create_test_data
    (1..6).inject(nil) { |prev, i| create(i, prev) }
    (1..6).each { |i| create_other(i) }
  end

  # Fixture-style accessor method to get CrudTestModel instances by name
  def crud_test_models(name)
    CrudTestModel.find_by_name(name.to_s)
  end

  def with_test_routing
    with_routing do |set|
      set.draw { resources :crud_test_models }
      # used to define a controller in these tests
      set.default_url_options = {controller: "crud_test_models"}
      yield
    end
  end

  def special_routing
    controller = @controller || controller  # test:unit uses instance variable, rspec the method
    @routes = ActionDispatch::Routing::RouteSet.new
    @routes.draw { resources :crud_test_models }

    _routes = @routes

    controller.singleton_class.send(:include, _routes.url_helpers)
    controller.view_context_class = Class.new(controller.view_context_class) do
      include _routes.url_helpers
    end
  end

  def create(index, companion) # rubocop:todo Metrics/AbcSize
    c = str(index)
    CrudTestModel.create!(name: c,
      children: 10 - index,
      companion: companion,
      rating: "#{index}.#{index}".to_f,
      income: 10_000_000 * index + 0.1 * index,
      birthdate: "#{1900 + 10 * index}-#{index}-#{index}",
      # store entire date to avoid time zone issues
      gets_up_at: Time.zone.local(2000, 1, 1, index, index),
      last_seen: Time.zone.local(2000 + 10 * index, index, index, 10 + index, 20 + index),
      human: index % 2 == 0,
      remarks: "#{c} #{str(index + 1)} #{str(index + 2)}\n" * (index % 3 + 1))
  end

  def create_other(index)
    c = str(index)
    others = CrudTestModel.all[index..(index + 2)]
    OtherCrudTestModel.create!(name: c, other_ids: others.collect(&:id), more_id: others.first.try(:id))
  end

  def str(index)
    (index + 64).chr * 5
  end

  # hack to avoid ddl in transaction issues with postgres.
  def without_transaction
    c = ActiveRecord::Base.connection
    start_transaction = false
    if c.adapter_name.downcase.include?("postgres") && c.open_transactions > 0
      # in transactional tests, we may simply rollback
      c.execute("ROLLBACK")
      start_transaction = true
    end

    yield

    c.execute("BEGIN") if start_transaction
  end
end
