require_relative '../client'

module Spaceship
  module TestFlight
    class Client < Spaceship::Client
      ##
      # Spaceship HTTP client for the testflight API.
      #
      # This client is solely responsible for the making HTTP requests and
      # parsing their responses. Parameters should be either named parameters, or
      # for large request data bodies, pass in anything that can respond to
      # `to_json`.
      #
      # Each request method should validate the required parameters. A required parameter is one that would result in 400-range response if it is not supplied.
      # Each request method should make only one request. For more high-level logic, put code in the data models.

      def self.hostname
        'https://appstoreconnect.apple.com/testflight/v2/'
      end

      ##
      # @!group Build trains API
      ##

      # Returns an array of all available build trains (not the builds they include)
      def get_build_trains(app_id: nil, platform: "ios")
        assert_required_params(__method__, binding)

        response = request(:get, "providers/#{team_id}/apps/#{app_id}/platforms/#{platform}/trains")

        handle_response(response)
      end

      def get_builds_for_train(app_id: nil, platform: "ios", train_version: nil, retry_count: 3)
        assert_required_params(__method__, binding)
        with_retry(retry_count) do
          response = request(:get, "providers/#{team_id}/apps/#{app_id}/platforms/#{platform}/trains/#{train_version}/builds", nil, {}, true)
          handle_response(response)
        end
      end

      ##
      # @!group Builds API
      ##

      def get_build(app_id: nil, build_id: nil)
        assert_required_params(__method__, binding)

        response = request(:get, "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}")
        handle_response(response)
      end

      def put_build(app_id: nil, build_id: nil, build: nil)
        assert_required_params(__method__, binding)

        response = request(:put) do |req|
          req.url("providers/#{team_id}/apps/#{app_id}/builds/#{build_id}")
          req.body = build.to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      def expire_build(app_id: nil, build_id: nil, build: nil)
        assert_required_params(__method__, binding)

        response = request(:post) do |req|
          req.url("providers/#{team_id}/apps/#{app_id}/builds/#{build_id}/expire")
          req.body = build.to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      ##
      # @!group Groups API
      ##

      # Returns a list of available testing groups
      # e.g.
      #   {"b6f65dbd-c845-4d91-bc39-0b661d608970" => "Boarding",
      #    "70402368-9deb-409f-9a26-bb3f215dfee3" => "Automatic"}
      def get_groups(app_id: nil)
        @cached_groups = {} unless @cached_groups

        return @cached_groups[app_id] if @cached_groups[app_id]
        assert_required_params(__method__, binding)

        response = request(:get, "/testflight/v2/providers/#{provider_id}/apps/#{app_id}/groups")
        @cached_groups[app_id] = handle_response(response)
      end

      def create_group_for_app(app_id: nil, group_name: nil)
        assert_required_params(__method__, binding)
        body = {
            'name' => group_name
        }

        response = request(:post) do |req|
          req.url("providers/#{team_id}/apps/#{app_id}/groups")
          req.body = body.to_json
          req.headers['Content-Type'] = 'application/json'
        end

        # This is invalid now.
        @cached_groups.delete(app_id) if @cached_groups

        handle_response(response)
      end

      def delete_group_for_app(app_id: nil, group_id: nil)
        assert_required_params(__method__, binding)
        url = "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}"
        response = request(:delete, url)
        handle_response(response)
      end

      def add_group_to_build(app_id: nil, group_id: nil, build_id: nil)
        assert_required_params(__method__, binding)

        body = {
          'groupId' => group_id,
          'buildId' => build_id
        }
        response = request(:put) do |req|
          req.url("providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/builds/#{build_id}")
          req.body = body.to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      #####################################################
      # @!group Testers
      #####################################################
      def testers(tester)
        url = tester.url[:index]
        r = request(:get, url)
        parse_response(r, 'data')['users']
      end

      def testers_by_app(tester, app_id, group_id: nil)
        if group_id.nil?
          group_ids = get_groups(app_id: app_id).map do |group|
            group['id']
          end
        end
        group_ids ||= [group_id]
        testers = []

        group_ids.each do |json_group_id|
          url = tester.url(app_id, provider_id, json_group_id)[:index_by_app]
          r = request(:get, url)
          testers += parse_response(r, 'data')['users']
        end

        testers
      end

      #####################################################
      # @!Internal Testers
      #####################################################
      def internal_users(app_id: nil)
        assert_required_params(__method__, binding)
        url = "providers/#{team_id}/apps/#{app_id}/internalUsers"
        r = request(:get, url)

        parse_response(r, 'data')
      end

      ##
      # @!group Testers API
      ##

      def testers_for_app(app_id: nil)
        assert_required_params(__method__, binding)
        page_size = 40 # that's enforced by the iTC servers
        resulting_array = []
        # Sort order is set to `default` instead of `email`, because any other sort order breaks pagination
        # when dealing with lots of anonymous (public link) testers: https://github.com/fastlane/fastlane/pull/13778
        initial_url = "providers/#{team_id}/apps/#{app_id}/testers?limit=#{page_size}&sort=default&order=asc"
        response = request(:get, initial_url)
        link_from_response = proc do |r|
          # I weep for Swift nil chaining
          (l = r.headers['link']) && (m = l.match(/<(.*)>/)) && m.captures.first
        end
        next_link = link_from_response.call(response)
        result = Array(handle_response(response))
        resulting_array += result
        return resulting_array if result.count == 0

        until next_link.nil?
          response = request(:get, next_link)
          result = Array(handle_response(response))
          next_link = link_from_response.call(response)

          break if result.count == 0

          resulting_array += result
        end
        return resulting_array.uniq
      end

      def delete_tester_from_app(app_id: nil, tester_id: nil)
        assert_required_params(__method__, binding)
        url = "providers/#{team_id}/apps/#{app_id}/testers/#{tester_id}"
        response = request(:delete, url)
        handle_response(response)
      end

      def remove_testers_from_testflight(app_id: nil, tester_ids: nil)
        assert_required_params(__method__, binding)
        url = "providers/#{team_id}/apps/#{app_id}/deleteTesters"
        response = request(:post) do |req|
          req.url(url)
          req.body = tester_ids.map { |i| { "id" => i } }.to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      def search_for_tester_in_app(app_id: nil, text: nil)
        assert_required_params(__method__, binding)
        text = CGI.escape(text)
        url = "providers/#{team_id}/apps/#{app_id}/testers?order=asc&search=#{text}&sort=status"
        response = request(:get, url)
        handle_response(response)
      end

      def resend_invite_to_external_tester(app_id: nil, tester_id: nil)
        assert_required_params(__method__, binding)
        url = "/testflight/v1/invites/#{app_id}/resend?testerId=#{tester_id}"
        response = request(:post, url)
        handle_response(response)
      end

      def create_app_level_tester(app_id: nil, first_name: nil, last_name: nil, email: nil)
        assert_required_params(__method__, binding)
        url = "providers/#{team_id}/apps/#{app_id}/testers"
        response = request(:post) do |req|
          req.url(url)
          req.body = {
            "email" => email,
            "firstName" => first_name,
            "lastName" => last_name
          }.to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      def post_tester_to_group(app_id: nil, email: nil, first_name: nil, last_name: nil, group_id: nil)
        assert_required_params(__method__, binding)

        # Then we can add the tester to the group that allows the app to test
        # This is easy enough, we already have all this data. We don't need any response from the previous request
        url = "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/testers"
        response = request(:post) do |req|
          req.url(url)
          req.body = [{
            "email" => email,
            "firstName" => first_name,
            "lastName" => last_name
          }].to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      def delete_tester_from_group(group_id: nil, tester_id: nil, app_id: nil)
        assert_required_params(__method__, binding)

        url = "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/testers/#{tester_id}"
        response = request(:delete) do |req|
          req.url(url)
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      def builds_for_group(app_id: nil, group_id: nil)
        assert_required_params(__method__, binding)

        url = "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/builds"
        response = request(:get, url)
        handle_response(response)
      end

      ##
      # @!group AppTestInfo
      ##

      def get_app_test_info(app_id: nil)
        assert_required_params(__method__, binding)

        response = request(:get, "providers/#{team_id}/apps/#{app_id}/testInfo")
        handle_response(response)
      end

      def put_app_test_info(app_id: nil, app_test_info: nil)
        assert_required_params(__method__, binding)

        response = request(:put) do |req|
          req.url("providers/#{team_id}/apps/#{app_id}/testInfo")
          req.body = app_test_info.to_json
          req.headers['Content-Type'] = 'application/json'
        end
        handle_response(response)
      end

      protected

      def handle_response(response)
        if (200...300).cover?(response.status) && (response.body.nil? || response.body.empty?)
          return
        end

        raise InternalServerError, "Server error got #{response.status}" if (500...600).cover?(response.status)

        unless response.body.kind_of?(Hash)
          raise UnexpectedResponse, response.body
        end

        raise UnexpectedResponse, response.body['error'] if response.body['error']

        raise UnexpectedResponse, "Temporary App Store Connect error: #{response.body}" if response.body['statusCode'] == 'ERROR'

        return response.body['data'] if response.body['data']

        return response.body
      end

      private

      # used to assert all of the named parameters are supplied values
      #
      # @raises NameError if the values are nil
      def assert_required_params(method_name, binding)
        parameter_names = method(method_name).parameters.map { |k, v| v }
        parameter_names.each do |name|
          if local_variable_get(binding, name).nil?
            raise NameError, "`#{name}' is a required parameter"
          end
        end
      end

      def local_variable_get(binding, name)
        if binding.respond_to?(:local_variable_get)
          binding.local_variable_get(name)
        else
          binding.eval(name.to_s)
        end
      end

      def provider_id
        return team_id if self.provider.nil?
        self.provider.provider_id
      end
    end
  end
end
