# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:user) { project.first_owner }
  let_it_be(:experiment) do
    create(:ml_experiments, project: project, user: user).tap do |e|
      create(:ml_candidates, experiment: e, user: user)
    end
  end

  let(:params) { basic_params }
  let(:ff_value) { true }
  let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } }
  let(:experiment_iid) { experiment.iid }
  let(:read_model_experiments) { true }
  let(:write_model_experiments) { true }

  before do
    allow(Ability).to receive(:allowed?).and_call_original
    allow(Ability).to receive(:allowed?)
                        .with(user, :read_model_experiments, project)
                        .and_return(read_model_experiments)
    allow(Ability).to receive(:allowed?)
                        .with(user, :write_model_experiments, project)
                        .and_return(write_model_experiments)

    sign_in(user)
  end

  shared_examples 'renders 404' do
    it 'renders 404' do
      expect(response).to have_gitlab_http_status(:not_found)
    end
  end

  shared_examples '404 if experiment does not exist' do
    context 'when experiment does not exist' do
      let(:experiment_iid) { non_existing_record_id }

      it_behaves_like 'renders 404'
    end
  end

  shared_examples 'requires read_model_experiments' do
    context 'when user does not have access' do
      let(:read_model_experiments) { false }

      it_behaves_like 'renders 404'
    end
  end

  describe 'GET index' do
    describe 'rendering' do
      before do
        list_experiments
      end

      it 'renders the template' do
        expect(response).to render_template('projects/ml/experiments/index')
      end
    end

    it_behaves_like 'requires read_model_experiments' do
      before do
        list_experiments
      end
    end
  end

  describe 'GET show' do
    describe 'html' do
      it 'renders the template' do
        show_experiment

        expect(response).to render_template('projects/ml/experiments/show')
      end

      describe 'pagination' do
        let_it_be(:candidates) do
          create_list(:ml_candidates, 5, experiment: experiment).tap do |c|
            c.first.metrics.create!(name: 'metric1', value: 0.3)
            c[1].metrics.create!(name: 'metric1', value: 0.2)
            c.last.metrics.create!(name: 'metric1', value: 0.6)
          end
        end

        let(:params) { basic_params.merge(id: experiment.iid) }

        before do
          stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2)

          show_experiment
        end

        it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do
          expect(assigns(:candidates).size).to eq(2)
        end

        it 'paginates' do
          received = assigns(:page_info)

          expect(received).to include({
            has_next_page: true,
            has_previous_page: false,
            start_cursor: nil
          })
        end

        context 'when order by metric' do
          let(:params) do
            {
              order_by: "metric1",
              order_by_type: "metric",
              sort: "desc"
            }
          end

          it 'paginates', :aggregate_failures do
            page = assigns(:candidates)

            expect(page.first).to eq(candidates.last)
            expect(page.last).to eq(candidates.first)

            new_params = params.merge(cursor: assigns(:page_info)[:end_cursor])

            show_experiment(new_params: new_params)

            new_page = assigns(:candidates)

            expect(new_page.first).to eq(candidates[1])
          end
        end
      end

      describe 'search' do
        let(:params) do
          basic_params.merge(
            name: 'some_name',
            orderBy: 'name',
            orderByType: 'metric',
            sort: 'asc',
            invalid: 'invalid'
          )
        end

        it 'formats and filters the parameters' do
          expect(Projects::Ml::CandidateFinder).to receive(:new).and_call_original do |exp, params|
            expect(params.to_h).to include({
              name: 'some_name',
              order_by: 'name',
              order_by_type: 'metric',
              sort: 'asc'
            })
          end

          show_experiment
        end
      end

      it 'does not perform N+1 sql queries' do
        control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment }

        create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment, project: experiment.project)

        expect { show_experiment }.not_to exceed_all_query_limit(control_count)
      end

      describe '404' do
        before do
          show_experiment
        end

        it_behaves_like '404 if experiment does not exist'
        it_behaves_like 'requires read_model_experiments'
      end
    end

    describe 'csv' do
      it 'responds with :ok', :aggregate_failures do
        show_experiment_csv

        expect(response).to have_gitlab_http_status(:ok)
        expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
      end

      it 'calls the presenter' do
        allow(::Ml::CandidatesCsvPresenter).to receive(:new).and_call_original

        show_experiment_csv
      end

      it 'does not perform N+1 sql queries' do
        control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment_csv }

        create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)

        expect { show_experiment_csv }.not_to exceed_all_query_limit(control_count)
      end

      describe '404' do
        before do
          show_experiment_csv
        end

        it_behaves_like '404 if experiment does not exist'
        it_behaves_like 'requires read_model_experiments'
      end
    end
  end

  describe 'DELETE #destroy' do
    let_it_be(:experiment_for_deletion) do
      create(:ml_experiments, project: project, user: user).tap do |e|
        create(:ml_candidates, experiment: e, user: user)
      end
    end

    let_it_be(:candidate_for_deletion) { experiment_for_deletion.candidates.first }

    let(:params) { basic_params.merge(id: experiment.iid) }

    before do
      destroy_experiment
    end

    it 'deletes the experiment' do
      expect { experiment.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end

    it_behaves_like '404 if experiment does not exist'

    describe 'requires write_model_experiments' do
      let(:write_model_experiments) { false }

      it 'is 404' do
        expect(response).to have_gitlab_http_status(:not_found)
      end
    end
  end

  private

  def show_experiment(new_params: nil, format: :html)
    get project_ml_experiment_path(project, experiment_iid, format: format), params: new_params || params
  end

  def show_experiment_csv
    show_experiment(format: :csv)
  end

  def list_experiments(new_params = nil)
    get project_ml_experiments_path(project), params: new_params || params
  end

  def destroy_experiment
    delete project_ml_experiment_path(project, experiment_iid), params: params
  end
end
