require 'api_test_helper'
require 'user_tokens_helper'
require 'test_helper'
require 'includes_helper'
require 'detached_helper'

class StudyFilesControllerTest < ActionDispatch::IntegrationTest

  before(:all) do
    Delayed::Worker.delay_jobs = false # run all .delay calls synchronously
    @user = FactoryBot.create(:api_user, test_array: @@users_to_clean)
    @other_user = FactoryBot.create(:api_user, test_array: @@users_to_clean)
    @study = FactoryBot.create(:detached_study,
                               name_prefix: 'StudyFile Study',
                               public: true,
                               user: @user,
                               test_array: @@studies_to_clean)
    @study_file = FactoryBot.create(:cluster_file, name: 'clusterA.txt', study: @study)
    @other_study_file = FactoryBot.create(:cluster_file, name: 'clusterB.txt', study: @study)
  end

  after(:all) do
    Delayed::Worker.delay_jobs = true # restore background job processing
  end

  setup do
    sign_in_and_update @user
  end

  teardown do
    OmniAuth.config.mock_auth[:google_oauth2] = nil
    reset_user_tokens
  end

  test 'should get index' do
    execute_http_request(:get, api_v1_study_study_files_path(@study))
    assert_response :success
    assert json.size >= 1, 'Did not find any study_files'
  end

  test 'should get study file' do
    execute_http_request(:get, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id))
    assert_response :success
    @study_file.reload # gotcha in case 'should parse study file' test has already run
    # check all attributes against database
    @study_file.attributes.each do |attribute, value|
      if attribute =~ /_id/
        assert json[attribute] == JSON.parse(value.to_json), "Attribute mismatch: #{attribute} is incorrect, expected #{JSON.parse(value.to_json)} but found #{json[attribute.to_s]}"
      elsif attribute =~ /_at/
        # ignore timestamps as formatting & drift on milliseconds can cause comparison errors
        next
      elsif attribute =~ /cluster_file_info/
        # ignore the $oid field and just look at custom_colors
        assert json[attribute]['custom_colors'] == value['custom_colors']
      else
        assert json[attribute] == value, "Attribute mismatch: #{attribute} is incorrect, expected #{value} but found #{json[attribute.to_s]}"
      end
    end

    # ensure other users cannot access study_file
    sign_in_and_update @other_user
    execute_http_request(:get, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id), user: @other_user)
    assert_response 403
  end

  # create, update & delete tested together to use new object to avoid delete/update running before create
  test 'should create then update then delete study file' do
    # create study file
    study_file_attributes = {
      study_file: {
        upload_file_name: 'table_1.xlsx',
        upload_content_type: 'application/octet-stream',
        upload_file_size: 41692,
        file_type: 'Other'
      }
    }
    execute_http_request(:post, api_v1_study_study_files_path(study_id: @study.id), request_payload: study_file_attributes)
    assert_response :success
    assert json['name'] == study_file_attributes[:study_file][:upload_file_name], "Did not set name correctly, expected #{study_file_attributes[:study_file][:upload_file_name]} but found #{json['name']}"
    # update study file
    study_file_id = json['_id']['$oid']
    update_attributes = {
      study_file: {
        description: "Test description #{SecureRandom.uuid}"
      }
    }
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: study_file_id), request_payload: update_attributes)
    assert_response :success
    assert json['description'] == update_attributes[:study_file][:description], "Did not set description correctly, expected #{update_attributes[:study_file][:description]} but found #{json['description']}"
    # delete study file
    execute_http_request(:delete, api_v1_study_study_file_path(study_id: @study.id, id: study_file_id))
    assert_response 204, "Did not successfully delete study file, expected response of 204 but found #{@response.response_code}"
  end

  test 'can update cluster name' do
    cluster_file = @study_file
    update_attributes = {
      study_file: {
        name: 'updated name'
      }
    }
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: cluster_file._id), request_payload: update_attributes)
    assert_equal 'updated name', json['name']
    assert_equal 'updated name', @study.cluster_groups.first.reload.name
  end


  # create a study file bundle using the study_files_controller method
  test 'should create study file bundle' do
    mock_not_detached @study, :find_by do
      study_file_bundle_attributes = {
        'files' => [
          { 'name' => 'cluster.tsv', 'file_type' => 'Cluster' },
          { 'name' => 'labels.tsv', 'file_type' => 'Coordinate Labels' }
        ]
      }
      execute_http_request(:post, api_v1_study_study_files_bundle_files_path(study_id: @study.id), request_payload: study_file_bundle_attributes)
      assert_response :success
      assert json['original_file_list'] == study_file_bundle_attributes['files'],
             "Did not set name correctly, expected #{study_file_bundle_attributes['files']} but found #{json['original_file_list']}"
      # delete study file bundle
      study_file_bundle_id = json['_id']['$oid']
      execute_http_request(:delete, api_v1_study_study_file_bundle_path(study_id: @study.id, id: study_file_bundle_id))
      assert_response 204, "Did not successfully delete study file bundle, expected response of 204 but found #{@response.response_code}"
    end
  end

  test 'should parse study file' do
    execute_http_request(:post, parse_api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id))
    assert_response 204
  end

  test 'should enforce edit access restrictions on study files' do
    sign_in_and_update @other_user
    description = "This is the updated description with random seed #{@random_seed}"
    update_attributes = {
      study_file: {
        description: description
      }
    }
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id.to_s),
                         request_payload: update_attributes, user: @other_user)
    assert_response 403
    @study_file.reload
    refute @study_file.description == description
  end

  # create, update & delete tested together to use new object to avoid delete/update running before create
  test 'should correctly update custom colors' do
    annot1_color_hash = {
      'annotation1': {
        'labelA': '#aabb00',
        'labelB': '#bb2211'
      }
    }.with_indifferent_access
    study_file_attributes = {
      study_file: {
        custom_color_updates: annot1_color_hash.to_json
      }
    }
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id),
      request_payload: study_file_attributes)
    assert_response :success

    @study_file.reload
    # check that the colors were updated
    assert_equal annot1_color_hash, @study_file.cluster_file_info.custom_colors_as_hash.with_indifferent_access

    annot2_color_hash = {
      'annotation2': {
        'labelA': '#aabb00',
        'labelC': '#bb2211'
      }
    }.with_indifferent_access
    study_file_attributes[:study_file][:custom_color_updates] = annot2_color_hash.to_json
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id),
      request_payload: study_file_attributes)
    assert_response :success

    @study_file.reload
    # check that the new annotation colors were added, and the previous ones remain
    assert_equal annot2_color_hash.merge(annot1_color_hash),
                 @study_file.cluster_file_info.custom_colors_as_hash.with_indifferent_access

    updated_annot2_color_hash = {
      'annotation1': {
        'labelA': '#333333',
        'labelC': '#999999'
      }
    }.with_indifferent_access
    study_file_attributes[:study_file][:custom_color_updates] = updated_annot2_color_hash.to_json
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id),
      request_payload: study_file_attributes)
    assert_response :success

    @study_file.reload
    # confirm the annotation1 colors were completely replaced, and annotation2 colors were preserved
    assert_equal updated_annot2_color_hash.merge(annot2_color_hash),
                 @study_file.cluster_file_info.custom_colors_as_hash.with_indifferent_access

    # test global update
    study_file_attributes[:study_file][:global_color_update] = true

    assert_nil @other_study_file.cluster_file_info
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: @study_file.id),
                         request_payload: study_file_attributes)
    assert_response :success
    # sleep 3 # let Delayed::Job process update in background
    @other_study_file.reload
    assert_equal updated_annot2_color_hash,
                 @other_study_file.cluster_file_info.custom_colors_as_hash.with_indifferent_access

  end

  test 'should create and update AnnData file' do
    cluster_frag_id = BSON::ObjectId.new.to_s
    exp_frag_id = BSON::ObjectId.new.to_s
    taxon_id = BSON::ObjectId.new.to_s
    ann_data_params = {
      study_file: {
        upload_file_name: 'data.h5ad',
        upload_content_type: 'application/octet-stream',
        upload_file_size: 1.megabyte,
        file_type: 'AnnData',
        reference_anndata_file: 'false',
        expression_file_info_attributes: {
          biosample_input_type: 'Whole cell',
          is_raw_counts: false,
          library_preparation_protocol: "10x 5' v3",
          modality: 'Transcriptomic: unbiased',
          raw_counts_associations: ['']
        },
        extra_expression_form_info_attributes: {
          _id: exp_frag_id,
          taxon_id: taxon_id,
          description: 'expression description',
          y_axis_label: 'log(TPM) expression'
        },
        metadata_form_info_attributes: {
          use_metadata_convention: false # check that override is in place to enforce convention
        },
        cluster_form_info_attributes: {
            _id: cluster_frag_id,
            name: 'UMAP',
            obsm_key_name: 'X_umap',
            description: 'UMAP cluster'
        }
      }
    }

    execute_http_request(:post,
                         api_v1_study_study_files_path(study_id: @study.id),
                         request_payload: ann_data_params)
    assert_response :success
    ann_data = StudyFile.find_by(study_id: @study.id, upload_file_name: 'data.h5ad')
    assert ann_data.present?
    assert ann_data.use_metadata_convention
    assert ann_data.ann_data_file_info.present?
    assert_not ann_data.ann_data_file_info.reference_file?
    data_fragments = ann_data.ann_data_file_info.data_fragments
    assert_equal 2, data_fragments.size
    # update file to test ann_data_file_info_attributes form structure
    update_attributes = {
      study_file: {
        description: 'updated',
        file_type: 'AnnData',
        ann_data_file_info_attributes: {
          data_fragments:
            [
              {
                _id: cluster_frag_id, data_type: 'cluster', name: 'UMAP', obsm_key_name: 'X_umap',
                description: 'updated'
              },
              {
                _id: exp_frag_id, data_type: 'expression', taxon_id: taxon_id, y_axis_label: 'log(TPM) expression',
                description: 'updated', expression_file_info: { biosample_input_type: 'Single nuclei' }
              }
            ].to_json
        }
      }
    }
    execute_http_request(:patch,
                         api_v1_study_study_file_path(study_id: @study.id, id: ann_data.id.to_s),
                         request_payload: update_attributes)
    assert_response :success
    ann_data.reload
    assert_equal 'updated', ann_data.description
    assert_equal 'log(TPM) expression', ann_data.ann_data_file_info.expression_axis_label
    ann_data.ann_data_file_info.data_fragments.each do |fragment|
      assert_equal 'updated', fragment[:description]
    end
    assert_equal 'Single nuclei', ann_data.expression_file_info.biosample_input_type
  end

  test 'should not reset parse status on update when using bucket path' do
    study_file = FactoryBot.create(:cluster_file,
                                   name: 'clusterNew.txt',
                                   study: @study)
    description = 'This is an update'
    study_file_attributes = {
      study_file: {
        description:,
        remote_location: 'clusterNew.txt'
      }
    }
    execute_http_request(:patch, api_v1_study_study_file_path(study_id: @study.id, id: study_file.id),
                         request_payload: study_file_attributes)
    assert_response :success
    study_file.reload
    assert_equal description, study_file.description
    assert study_file.parsed?
  end

  test 'should extract only raw counts from existing AnnData uploads' do
    study = FactoryBot.create(:detached_study,
                              name_prefix: 'AnnData Raw Counts Study',
                              public: true,
                              user: @user,
                              test_array: @@studies_to_clean)
    ann_data_file = FactoryBot.create(:ann_data_file,
                                      name: 'matrix.h5ad',
                                      study:,
                                      upload_file_size: 1.megabyte,
                                      cell_input: %w[A B C D],
                                      has_raw_counts: false,
                                      reference_file: false,
                                      annotation_input: [
                                        { name: 'disease', type: 'group', values: %w[cancer cancer normal normal] }
                                      ],
                                      coordinate_input: [
                                        { umap: { x: [1, 2, 3, 4], y: [5, 6, 7, 8] } }
                                      ],
                                      expression_input: {
                                        'phex' => [['A', 0.3], ['B', 1.0], ['C', 0.5], ['D', 0.1]]
                                      })

    assert_not ann_data_file.is_raw_counts_file?
    assert_not ann_data_file.needs_raw_counts_extraction?

    # test for normal uploads
    study_file_attributes = {
      study_file: {
        expression_file_info_attributes: {
          is_raw_counts: true,
          units: 'raw counts'
        },
        ann_data_file_info_attributes: {
          raw_location: '.raw'
        }
      }
    }
    FileParseService.stub :run_parse_job, true do
      execute_http_request(:patch, api_v1_study_study_file_path(study_id: study.id, id: ann_data_file.id),
                           request_payload: study_file_attributes)
      assert_response :success
      ann_data_file.reload
      assert ann_data_file.is_raw_counts_file?
      assert ann_data_file.needs_raw_counts_extraction?
    end

    # test for bucket path uploads, resetting flags first
    ann_data_file.expression_file_info.update(is_raw_counts: false)
    ann_data_file.ann_data_file_info.update(has_raw_counts: false)
    ann_data_file.update(remote_location: 'matrix.h5ad')
    assert_not ann_data_file.is_raw_counts_file?
    assert_not ann_data_file.needs_raw_counts_extraction?

    FileParseService.stub :run_parse_job, true do
      execute_http_request(:patch, api_v1_study_study_file_path(study_id: study.id, id: ann_data_file.id),
                           request_payload: study_file_attributes)
      assert_response :success
      ann_data_file.reload
      assert ann_data_file.is_raw_counts_file?
      assert ann_data_file.needs_raw_counts_extraction?
    end
  end
end
