lib/fbe/octo.rb



# frozen_string_literal: true

# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
# SPDX-License-Identifier: MIT

require 'decoor'
require 'ellipsized'
require 'faraday/http_cache'
require 'faraday/retry'
require 'filesize'
require 'intercepted'
require 'json'
require 'loog'
require 'obk'
require 'octokit'
require 'others'
require 'tago'
require 'uri'
require 'verbose'
require_relative '../fbe'
require_relative 'middleware'
require_relative 'middleware/formatter'
require_relative 'middleware/rate_limit'
require_relative 'middleware/sqlite_store'
require_relative 'middleware/trace'

# Makes a call to the GitHub API.
#
# It is supposed to be used instead of +Octokit::Client+, because it
# is pre-configured and enables additional features, such as retrying,
# logging, and caching.
#
# @param [Judges::Options] options The options available globally
# @option options [String] :github_token GitHub API token for authentication
# @option options [Boolean] :testing When true, uses FakeOctokit for testing
# @option options [String] :sqlite_cache Path to SQLite cache file for HTTP responses
# @option options [Integer] :sqlite_cache_maxsize Maximum size of SQLite cache in bytes (default: 10MB)
# @param [Hash] global Hash of global options
# @param [Loog] loog Logging facility
# @return [Hash] Usually returns a JSON, as it comes from the GitHub API
def Fbe.octo(options: $options, global: $global, loog: $loog)
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  global[:octo] ||=
    begin
      trace = []
      if options.testing.nil?
        o = Octokit::Client.new
        token = options.github_token
        if token.nil?
          loog.debug("The 'github_token' option is not provided")
          token = ENV.fetch('GITHUB_TOKEN', nil)
          if token.nil?
            loog.debug("The 'GITHUB_TOKEN' environment variable is not set")
          else
            loog.debug("The 'GITHUB_TOKEN' environment was provided")
          end
        else
          loog.debug("The 'github_token' option was provided (#{token.length} chars)")
        end
        if token.nil?
          loog.warn('Accessing GitHub API without a token!')
        elsif token.empty?
          loog.warn('The GitHub API token is an empty string, won\'t use it')
        else
          o = Octokit::Client.new(access_token: token)
        end
        o.auto_paginate = true
        o.per_page = 100
        o.connection_options = {
          request: {
            open_timeout: 15,
            timeout: 15
          }
        }
        stack =
          Faraday::RackBuilder.new do |builder|
            builder.use(
              Faraday::Retry::Middleware,
              exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [
                Octokit::TooManyRequests, Octokit::ServiceUnavailable
              ],
              max: 4,
              interval: ENV['RACK_ENV'] == 'test' ? 0.01 : 4,
              methods: [:get],
              backoff_factor: 2
            )
            builder.use(Octokit::Response::RaiseError)
            builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
            builder.use(Fbe::Middleware::RateLimit)
            builder.use(Fbe::Middleware::Trace, trace, ignores: [:fresh])
            if options.sqlite_cache
              maxsize = Filesize.from(options.sqlite_cache_maxsize || '100M').to_i
              maxvsize = Filesize.from(options.sqlite_cache_maxvsize || '100K').to_i
              cache_min_age = options.sqlite_cache_min_age&.to_i
              store = Fbe::Middleware::SqliteStore.new(
                options.sqlite_cache, Fbe::VERSION, loog:, maxsize:, maxvsize:, ttl: 24, cache_min_age:
              )
              loog.info(
                "Using HTTP cache in SQLite file: #{store.path} (" \
                "#{File.exist?(store.path) ? Filesize.from(File.size(store.path).to_s).pretty : 'file is absent'}, " \
                "max size: #{Filesize.from(maxsize.to_s).pretty}, max vsize: #{Filesize.from(maxvsize.to_s).pretty})"
              )
              builder.use(
                Faraday::HttpCache,
                store:, serializer: JSON, shared_cache: false, logger: Loog::NULL
              )
            else
              loog.info("No HTTP cache in SQLite file, because 'sqlite_cache' option is not provided")
              builder.use(
                Faraday::HttpCache,
                serializer: Marshal, shared_cache: false, logger: Loog::NULL
              )
            end
            builder.adapter(Faraday.default_adapter)
          end
        o.middleware = stack
        o = Verbose.new(o, log: loog)
        unless token.nil? || token.empty?
          loog.info(
            "Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \
            "#{o.rate_limit.remaining} quota remaining)"
          )
        end
      else
        loog.debug('The connection to GitHub API is mocked')
        o = Fbe::FakeOctokit.new
      end
      o =
        decoor(o, loog:, trace:) do
          def print_trace!(all: false, max: 5)
            if @trace.empty?
              @loog.debug('GitHub API trace is empty')
            else
              grouped =
                @trace.select { |e| e[:duration] > 0.05 || all }.group_by do |entry|
                  uri = URI.parse(entry[:url])
                  query = uri.query
                  query = "?#{query.ellipsized(40)}" if query
                  "#{uri.scheme}://#{uri.host}#{uri.path}#{query}"
                end
              message = grouped
                .sort_by { |_path, entries| -entries.count }
                .map do |path, entries|
                  [
                    '  ',
                    path.gsub(%r{^https://api.github.com/}, '/'),
                    ': ',
                    entries.count,
                    " (#{entries.sum { |e| e[:duration] }.seconds})"
                  ].join
                end
                .take(max)
                .join("\n")
              @loog.info(
                "GitHub API trace (#{grouped.count} URLs vs #{@trace.count} requests, " \
                "#{@origin.rate_limit!.remaining} quota left):\n#{message}"
              )
              @trace.clear
            end
          end

          def off_quota?(threshold: 50)
            left = @origin.rate_limit!.remaining
            if left < threshold
              @loog.info("Too much GitHub API quota consumed already (#{left} < #{threshold}), stopping")
              true
            else
              @loog.debug("Still #{left} GitHub API quota left (>#{threshold})")
              false
            end
          end

          def user_name_by_id(id)
            raise 'The ID of the user is nil' if id.nil?
            raise 'The ID of the user must be an Integer' unless id.is_a?(Integer)
            json = @origin.user(id)
            name = json[:login].downcase
            @loog.debug("GitHub user ##{id} has a name: @#{name}")
            name
          end

          def repo_id_by_name(name)
            raise 'The name of the repo is nil' if name.nil?
            json = @origin.repository(name)
            id = json[:id]
            @loog.debug("GitHub repository #{name.inspect} has an ID: ##{id}")
            id
          end

          def repo_name_by_id(id)
            raise 'The ID of the repo is nil' if id.nil?
            raise 'The ID of the repo must be an Integer' unless id.is_a?(Integer)
            json = @origin.repository(id)
            name = json[:full_name].downcase
            @loog.debug("GitHub repository ##{id} has a name: #{name}")
            name
          end
        end
      o =
        intercepted(o) do |e, m, _args, _r|
          if e == :before && m != :off_quota? && m != :print_trace! && m != :rate_limit && o.off_quota?
            raise "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{name}()"
          end
        end
      o
    end
end

# Fake GitHub client for testing purposes.
#
# This class provides mock implementations of Octokit methods for testing.
# It returns predictable, deterministic data structures that mimic GitHub API
# responses without making actual API calls. The mock data uses consistent
# patterns:
# - IDs are generated from string names using character code sums
# - Timestamps are random but within recent past
# - Repository and user data follows GitHub's JSON structure
#
# @example Using FakeOctokit in tests
#   client = Fbe::FakeOctokit.new
#   repo = client.repository('octocat/hello-world')
#   puts repo[:full_name]  # => "octocat/hello-world"
#   puts repo[:id]         # => 1224 (deterministic from name)
#
# @note All methods return static or pseudo-random data
# @note No actual API calls are made
class Fbe::FakeOctokit
  # Generates a random time in the past.
  #
  # @return [Time] A random time within the last 10,000 seconds
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   time = fake_client.random_time #=> 2024-09-04 12:34:56 -0700
  def random_time
    Time.now - rand(10_000)
  end

  # Converts a string name to a deterministic integer.
  #
  # @param [String, Integer] name The name to convert or pass through
  # @return [Integer, String] The sum of character codes if input is a string, otherwise the original input
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.name_to_number("octocat") #=> 728
  #   fake_client.name_to_number(42) #=> 42
  def name_to_number(name)
    return name unless name.is_a?(String)
    name.chars.sum(&:ord)
  end

  # Returns a mock rate limit object.
  #
  # @return [Object] An object with a remaining method that returns 100
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.rate_limit.remaining #=> 100
  def rate_limit
    o = Object.new
    def o.remaining
      100
    end
    o
  end
  alias rate_limit! rate_limit

  # Lists repositories for a user or organization.
  #
  # @param [String] _user The user/org name (ignored in mock)
  # @return [Array<Hash>] Array of repository hashes
  # @example
  #   client.repositories('octocat')
  #   # => [{:id=>123, :full_name=>"yegor256/judges", ...}, ...]
  def repositories(_user = nil)
    [
      repository('yegor256/judges'),
      repository('yegor256/factbase')
    ]
  end

  # Gets repository invitations for the authenticated user.
  #
  # @param [Hash] _options Additional options (not used in mock)
  # @return [Array<Hash>] Array of invitation objects with repository and inviter information
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.user_repository_invitations #=> [{:id=>1, :node_id=>"INV_", ...}]
  def user_repository_invitations(_options = {})
    [
      {
        id: 1,
        node_id: 'INV_kwDOJRF-Hq4B_yXr',
        repository: repository('zerocracy/fbe'),
        invitee: user(526_301),
        inviter: user(888),
        permissions: 'write',
        created_at: random_time,
        url: 'https://api.github.com/user/repository_invitations/1',
        html_url: 'https://github.com/zerocracy/fbe/invitations',
        expired: false
      },
      {
        id: 2,
        node_id: 'INV_kwDOJRF-Hq4B_yXs',
        repository: repository('yegor256/takes'),
        invitee: user(526_301),
        inviter: user(888),
        permissions: 'admin',
        created_at: random_time,
        url: 'https://api.github.com/user/repository_invitations/2',
        html_url: 'https://github.com/yegor256/takes/invitations',
        expired: false
      }
    ]
  end

  # Gets organization memberships for the authenticated user.
  #
  # @param [Hash] _options Additional options (not used in mock)
  # @return [Array<Hash>] Array of organization membership objects
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.organization_memberships #=> [{:url=>"https://api.github.com/orgs/...", ...}]
  def organization_memberships(_options = {})
    [
      {
        url: 'https://api.github.com/orgs/zerocracy/memberships/yegor256',
        state: 'active',
        role: 'admin',
        organization_url: 'https://api.github.com/orgs/zerocracy',
        organization: {
          login: 'zerocracy',
          id: 24_234_201,
          node_id: 'MDEyOk9yZ2FuaXphdGlvbjI0MjM0MjAx',
          url: 'https://api.github.com/orgs/zerocracy',
          avatar_url: 'https://avatars.githubusercontent.com/u/24234201?v=4',
          description: 'AI-managed software development',
          name: 'Zerocracy',
          company: nil,
          blog: 'https://www.zerocracy.com',
          location: nil,
          email: 'team@zerocracy.com',
          twitter_username: nil,
          is_verified: false,
          has_organization_projects: true,
          has_repository_projects: true,
          public_repos: 30,
          public_gists: 0,
          followers: 0,
          following: 0,
          html_url: 'https://github.com/zerocracy',
          created_at: random_time,
          updated_at: random_time,
          type: 'Organization'
        },
        user: user(526_301)
      },
      {
        url: 'https://api.github.com/orgs/objectionary/memberships/yegor256',
        state: 'active',
        role: 'member',
        organization_url: 'https://api.github.com/orgs/objectionary',
        organization: {
          login: 'objectionary',
          id: 80_033_603,
          node_id: 'MDEyOk9yZ2FuaXphdGlvbjgwMDMzNjAz',
          url: 'https://api.github.com/orgs/objectionary',
          avatar_url: 'https://avatars.githubusercontent.com/u/80033603?v=4',
          description: 'EO/EOLANG, an object-oriented language',
          name: 'Objectionary',
          company: nil,
          blog: 'https://www.eolang.org',
          location: nil,
          email: nil,
          twitter_username: nil,
          is_verified: false,
          has_organization_projects: true,
          has_repository_projects: true,
          public_repos: 15,
          public_gists: 0,
          followers: 0,
          following: 0,
          html_url: 'https://github.com/objectionary',
          created_at: random_time,
          updated_at: random_time,
          type: 'Organization'
        },
        user: user(526_301)
      }
    ]
  end

  # Updates the authenticated user's organization membership.
  #
  # @param [String] org The organization name (e.g., 'zerocracy')
  # @param [Hash] _options Additional options (typically includes :state to update membership state)
  # @return [Hash] Updated membership information
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.update_organization_membership('zerocracy', state: 'active')
  def update_organization_membership(org, _options = {})
    {
      url: "https://api.github.com/orgs/#{org}/memberships/yegor256",
      state: 'active',
      role: 'member',
      organization_url: "https://api.github.com/orgs/#{org}",
      organization: {
        login: org,
        id: 24_234_201,
        node_id: 'MDEyOk9yZ2FuaXphdGlvbjI0MjM0MjAx',
        url: "https://api.github.com/orgs/#{org}",
        avatar_url: 'https://avatars.githubusercontent.com/u/24234201?v=4',
        description: 'Organization description',
        name: org.capitalize,
        company: nil,
        blog: "https://www.#{org}.com",
        location: nil,
        email: "team@#{org}.com",
        twitter_username: nil,
        is_verified: false,
        has_organization_projects: true,
        has_repository_projects: true,
        public_repos: 30,
        public_gists: 0,
        followers: 0,
        following: 0,
        html_url: "https://github.com/#{org}",
        created_at: random_time,
        updated_at: random_time,
        type: 'Organization'
      },
      user: user(526_301)
    }
  end

  # Removes a user from an organization.
  #
  # @param [String] _org The organization name (e.g., 'zerocracy')
  # @param [String] _user The user login (not used in this mock implementation)
  # @return [Boolean] Returns true when successful (204 No Content in actual API)
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.remove_organization_membership('zerocracy') #=> true
  # rubocop:disable Naming/PredicateMethod
  def remove_organization_membership(_org, _user = nil)
    true
  end
  # rubocop:enable Naming/PredicateMethod

  # Accepts a repository invitation.
  #
  # @param [Integer] id The invitation ID
  # @return [Boolean] Returns true when successful (204 No Content in actual API)
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.accept_repository_invitation(1) #=> true
  # rubocop:disable Naming/PredicateMethod
  def accept_repository_invitation(id)
    raise Octokit::NotFound if id == 404_000
    true
  end
  # rubocop:enable Naming/PredicateMethod

  # Gives a star to a repository.
  #
  # @param [String] _repo The repository name (e.g., 'user/repo')
  # @return [Boolean] Always returns true
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.star('octocat/Hello-World') #=> true
  # rubocop:disable Naming/PredicateMethod
  def star(_repo)
    true
  end
  # rubocop:enable Naming/PredicateMethod

  # Gets details of a GitHub user.
  #
  # @param [String, Integer] uid The login of the user or its numeric ID
  # @return [Hash] User information including id, login, and type
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   fake_client.user(526_301) #=> {:id=>444, :login=>"yegor256", :type=>"User"}
  #   fake_client.user('octocat') #=> {:id=>444, :login=>nil, :type=>"User"}
  def user(uid)
    raise Octokit::NotFound if [404_001, 404_002].include?(uid)
    login = (uid == 526_301 ? 'yegor256' : 'torvalds') if uid.is_a?(Integer)
    {
      id: 444,
      login:,
      type: uid == 29_139_614 ? 'Bot' : 'User'
    }
  end

  # Gets workflow runs for a repository.
  #
  # @param [String] repo The repository name
  # @param [Hash] _opts Additional options (not used in mock)
  # @return [Hash] Information about workflow runs including counts and details
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   result = fake_client.repository_workflow_runs('octocat/Hello-World')
  #   result[:total_count] #=> 2
  def repository_workflow_runs(repo, _opts = {})
    {
      total_count: 2,
      workflow_runs: [
        workflow_run(repo, 42),
        workflow_run(repo, 7)
      ]
    }
  end

  # Gets usage information for a specific workflow run.
  #
  # @param [String] _repo The repository name
  # @param [Integer] _id The workflow run ID
  # @return [Hash] Billing and usage information for the workflow run
  # @example
  #   fake_client = Fbe::FakeOctokit.new
  #   usage = fake_client.workflow_run_usage('octocat/Hello-World', 42)
  #   usage[:run_duration_ms] #=> 53000
  def workflow_run_usage(_repo, _id)
    {
      billable: {
        UBUNTU: {
          total_ms: 0,
          jobs: 1,
          job_runs: [
            {
              job_id: 1,
              duration_ms: 0
            }
          ]
        }
      },
      run_duration_ms: 53_000
    }
  end

  # Lists releases for a repository.
  #
  # @param [String] _repo Repository name (ignored in mock)
  # @param [Hash] _opts Options hash (ignored in mock)
  # @return [Array<Hash>] Array of release hashes
  # @example
  #   client.releases('octocat/Hello-World')
  #   # => [{:tag_name=>"0.19.0", :name=>"just a fake name", ...}, ...]
  def releases(_repo, _opts = {})
    [
      release('https://github...'),
      release('https://gith')
    ]
  end

  # Gets a single release.
  #
  # @param [String] _url Release URL (ignored in mock)
  # @return [Hash] Release information
  # @example
  #   client.release('https://api.github.com/repos/octocat/Hello-World/releases/1')
  #   # => {:tag_name=>"0.19.0", :name=>"just a fake name", ...}
  def release(_url)
    {
      node_id: 'RE_kwDOL6GCO84J7Cen',
      tag_name: '0.19.0',
      target_commitish: 'master',
      name: 'just a fake name',
      draft: false,
      prerelease: false,
      created_at: random_time,
      published_at: random_time,
      assets: []
    }
  end

  # Gets repository information.
  #
  # @param [String, Integer] name Repository name ('owner/repo') or ID
  # @return [Hash] Repository information
  # @raise [Octokit::NotFound] If name is 404123 or 404124 (for testing)
  # @example
  #   client.repository('octocat/Hello-World')
  #   # => {:id=>1296269, :full_name=>"octocat/Hello-World", ...}
  def repository(name)
    raise Octokit::NotFound if [404_123, 404_124].include?(name)
    {
      id: name_to_number(name),
      full_name: name.is_a?(Integer) ? 'yegor256/test' : name,
      default_branch: 'master',
      private: false,
      owner: { login: name.to_s.split('/')[0], id: 526_301, site_admin: false },
      html_url: "https://github.com/#{name}",
      description: 'something',
      fork: false,
      url: "https://github.com/#{name}",
      created_at: random_time,
      updated_at: random_time,
      pushed_at: random_time,
      size: name == 'yegor256/empty-repo' ? 0 : 470,
      stargazers_count: 1,
      watchers_count: 1,
      language: 'Ruby',
      has_issues: true,
      has_projects: true,
      has_downloads: true,
      has_wiki: true,
      has_pages: false,
      has_discussions: false,
      forks_count: 0,
      archived: name == 'zerocracy/datum',
      disabled: false,
      open_issues_count: 6,
      license: { key: 'mit', name: 'MIT License' },
      allow_forking: true,
      is_template: false,
      visibility: 'public',
      forks: 0,
      open_issues: 6,
      watchers: 1
    }
  end

  # Lists pull requests associated with a commit.
  #
  # @param [String] repo Repository name ('owner/repo')
  # @param [String] _sha Commit SHA (ignored in mock)
  # @return [Array<Hash>] Array of pull request hashes
  # @example
  #   client.commit_pulls('octocat/Hello-World', 'abc123')
  #   # => [{:number=>42, :state=>"open", ...}]
  def commit_pulls(repo, _sha)
    [
      pull_request(repo, 42)
    ]
  end

  # Lists issues for a repository.
  #
  # @param [String] repo Repository name ('owner/repo')
  # @param [Hash] _options Query options (ignored in mock)
  # @return [Array<Hash>] Array of issue hashes
  # @example
  #   client.list_issues('octocat/Hello-World', state: 'open')
  #   # => [{:number=>42, :title=>"Found a bug", ...}, ...]
  def list_issues(repo, _options = {})
    [
      issue(repo, 42),
      issue(repo, 43)
    ]
  end

  # Gets a single issue.
  #
  # @param [String] repo Repository name ('owner/repo')
  # @param [Integer] number Issue number
  # @return [Hash] Issue information
  # @example
  #   client.issue('octocat/Hello-World', 42)
  #   # => {:id=>42, :number=>42, :created_at=>...}
  def issue(repo, number)
    if number == 142
      {
        id: 655,
        number:,
        repo: { full_name: repo },
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        created_at: Time.parse('2025-06-01 12:00:55 UTC'),
        updated_at: Time.parse('2025-06-01 15:47:18 UTC'),
        closed_at: Time.parse('2025-06-02 15:00:00 UTC'),
        closed_by: { id: 526_301, login: 'yegor256' }
      }
    elsif number == 143
      {
        id: 656,
        number:,
        repo: { full_name: repo },
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        pull_request: { merged_at: nil },
        created_at: Time.parse('2025-05-29 17:00:55 UTC'),
        updated_at: Time.parse('2025-05-29 19:00:00 UTC'),
        closed_at: Time.parse('2025-06-01 18:20:00 UTC'),
        closed_by: { id: 526_301, login: 'yegor256' }
      }
    else
      {
        id: 42,
        number:,
        repo: {
          full_name: repo
        },
        pull_request: {
          merged_at: nil
        },
        created_at: Time.parse('2024-09-20 19:00:00 UTC')
      }
    end
  end

  # Gets a single pull request.
  #
  # @param [String] repo Repository name ('owner/repo')
  # @param [Integer] number Pull request number
  # @return [Hash] Pull request information
  # @example
  #   client.pull_request('octocat/Hello-World', 1)
  #   # => {:id=>42, :number=>1, :additions=>12, ...}
  def pull_request(repo, number)
    {
      id: 42,
      number:,
      repo: {
        full_name: repo
      },
      base: {
        repo: {
          full_name: repo
        }
      },
      state: 'closed',
      user: { login: 'yegor256', id: 526_301, type: 'User' },
      head: { ref: 'master', sha: '6dcb09b5b57875f334f61aebed695e2e4193db5e' },
      additions: 12,
      deletions: 5,
      changed_files: 3,
      comments: 2,
      review_comments: 2,
      closed_at: Time.parse('2024-12-20'),
      merged_at: Time.parse('2024-12-20'),
      created_at: Time.parse('2024-09-20')
    }
  end

  # Lists pull requests for a repository.
  #
  # @param [String] _repo Repository name (ignored in mock)
  # @param [Hash] _options Query options (ignored in mock)
  # @return [Array<Hash>] Array of pull request hashes
  # @example
  #   client.pull_requests('octocat/Hello-World', state: 'open')
  #   # => [{:number=>100, :state=>"closed", :title=>"#90: some title", ...}]
  def pull_requests(_repo, _options = {})
    [
      {
        id: 2_072_543_250,
        number: 100,
        state: 'closed',
        locked: false,
        title: '#90: some title',
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        body: 'Closes #90',
        created_at: Time.parse('2024-09-15 09:32:49 UTC'),
        updated_at: Time.parse('2024-09-15 10:06:23 UTC'),
        closed_at: Time.parse('2024-09-15 10:05:34 UTC'),
        merged_at: Time.parse('2024-09-15 10:05:34 UTC'),
        merge_commit_sha: '0527cc188b0495e',
        draft: false,
        head: {
          label: 'yegor256:90',
          ref: '90',
          sha: '0527cc188b049',
          user: { login: 'yegor256', id: 526_301, type: 'User' },
          repo: repository('yegor256/repo')
        },
        base: {
          label: 'zerocracy:master',
          ref: 'master',
          sha: '4643eb3c7a0ccb3c',
          user: { login: 'zerocracy', id: 24_234_201, type: 'Organization' },
          repo: repository('zerocracy/repo')
        }
      },
      {
        id: 2_072_543_240,
        number: 95,
        state: 'open',
        locked: false,
        title: '#80: some title',
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        body: 'Closes #80',
        created_at: Time.parse('2024-09-14 09:32:49 UTC'),
        updated_at: Time.parse('2024-09-14 10:06:23 UTC'),
        closed_at: nil,
        merged_at: nil,
        merge_commit_sha: '0627cc188b0497e',
        draft: false,
        head: {
          label: 'yegor256:80',
          ref: '80',
          sha: '1527cc188b040',
          user: { login: 'yegor256', id: 526_301, type: 'User' },
          repo: repository('yegor256/repo')
        },
        base: {
          label: 'zerocracy:master',
          ref: 'master',
          sha: '5643eb3c7a0ccb3b',
          user: { login: 'zerocracy', id: 24_234_201, type: 'Organization' },
          repo: repository('zerocracy/repo')
        }
      }
    ]
  end

  def pull_request_reviews(_repo, _number)
    [
      {
        id: 22_449_327,
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        body: 'Some text 2',
        state: 'CHANGES_REQUESTED',
        author_association: 'CONTRIBUTOR',
        submitted_at: Time.parse('2024-08-22 10:00:00 UTC'),
        commit_id: 'b15c2893f1b5453'
      },
      {
        id: 22_449_326,
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        body: 'Some text 1',
        state: 'CHANGES_REQUESTED',
        author_association: 'CONTRIBUTOR',
        submitted_at: Time.parse('2024-08-21 22:00:00 UTC'),
        commit_id: 'a15c2893f1b5453'
      }
    ]
  end

  def review_comments(_repo, _number)
    [
      {
        pull_request_review_id: 22_687_249,
        id: 17_361_949,
        body: 'Some comment 1',
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        created_at: Time.parse('2024-09-05 15:31:06 UTC'),
        updated_at: Time.parse('2024-09-05 15:33:04 UTC')
      },
      {
        pull_request_review_id: 22_687_503,
        id: 17_361_950,
        body: 'Some comment 2',
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        created_at: Time.parse('2024-09-06 14:20:00 UTC'),
        updated_at: Time.parse('2024-09-06 14:20:50 UTC')
      },
      {
        pull_request_review_id: 22_687_255,
        id: 17_361_970,
        body: 'Some comment 3',
        user: { login: 'yegor256', id: 526_301, type: 'User' },
        created_at: Time.parse('2024-09-06 20:45:30 UTC'),
        updated_at: Time.parse('2024-09-06 20:45:30 UTC')
      }
    ]
  end

  def add_comment(_repo, _issue, _text)
    {
      id: 42
    }
  end

  def create_commit_comment(_repo, sha, text)
    {
      commit_id: sha,
      id: 42,
      body: text,
      path: 'something.txt',
      line: 1,
      position: 1
    }
  end

  def search_issues(query, _options = {})
    if query.include?('type:pr') && query.include?('is:unmerged')
      {
        total_count: 1,
        incomplete_results: false,
        items: [
          {
            id: 42,
            number: 10,
            title: 'Awesome PR 10'
          }
        ]
      }
    elsif query.include?('type:pr') && query.include?('is:merged')
      {
        total_count: 1,
        incomplete_results: false,
        items: [
          {
            id: 42,
            number: 10,
            title: 'Awesome PR 10',
            created_at: Time.parse('2024-08-21 19:00:00 UTC'),
            pull_request: { merged_at: Time.parse('2024-08-23 19:00:00 UTC') }
          }
        ]
      }
    elsif query.include?('type:pr')
      {
        total_count: 2,
        incomplete_results: false,
        items: [
          {
            id: 42,
            number: 10,
            title: 'Awesome PR 10',
            created_at: Time.parse('2024-08-21 19:00:00 UTC')
          },
          {
            id: 43,
            number: 11,
            title: 'Awesome PR 11',
            created_at: Time.parse('2024-08-21 20:00:00 UTC')
          }
        ]
      }
    else
      {
        total_count: 1,
        incomplete_results: false,
        items: [
          {
            number: 42,
            labels: [
              {
                name: 'bug'
              }
            ],
            user: { login: 'yegor256', id: 526_301, type: 'User' },
            created_at: Time.parse('2024-08-20 19:00:00 UTC')
          }
        ]
      }
    end
  end

  def commits_since(repo, _since)
    [
      commit(repo, 'a1b2c3d4e5f6a1b2c3d4e5f6'),
      commit(repo, 'a1b2c3d4e5fff1b2c3d4e5f6')
    ]
  end

  def commit(_repo, sha)
    {
      sha:,
      stats: {
        total: 123
      }
    }
  end

  def search_commits(_query, _options = {})
    {
      total_count: 3,
      incomplete_results: false,
      items: [
        {
          commit: {
            author: { name: 'Yegor', email: 'yegor@gmail.com', date: Time.parse('2024-09-15 12:23:25 UTC') },
            committer: { name: 'Yegor', email: 'yegor@gmail.com', date: Time.parse('2024-09-15 12:23:25 UTC') },
            message: 'Some text',
            tree: { sha: '6e04579960bf67610d' },
            comment_count: 0
          },
          author: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
          committer: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
          parents: [{ sha: '60cff20bdb66' }],
          repository: {
            id: 799_177_290, name: 'judges-action', full_name: 'zerocracy/judges-action',
            owner: { login: 'zerocracy', id: 24_234_201, type: 'Organization', site_admin: false }
          }
        },
        {
          commit: {
            author: { name: 'Yegor', email: 'yegor2@gmail.com', date: Time.parse('2024-09-14 12:23:25 UTC') },
            committer: { name: 'Yegor', email: 'yegor2@gmail.com', date: Time.parse('2024-09-14 12:23:25 UTC') },
            message: 'Some text 2',
            tree: { sha: 'defa18e4e2250987' },
            comment_count: 0
          },
          author: { login: 'yegor257', id: 526_302, type: 'User', site_admin: false },
          committer: { login: 'yegor257', id: 526_302, type: 'User', site_admin: false },
          parents: [{ sha: 'a04c15bb34fddbba' }],
          repository: {
            id: 799_177_290, name: 'judges-action', full_name: 'zerocracy/judges-action',
            owner: { login: 'zerocracy', id: 24_234_201, type: 'Organization', site_admin: false }
          }
        },
        {
          commit: {
            author: { name: 'Yegor', email: 'yegor3@gmail.com', date: Time.parse('2024-09-13 12:23:25 UTC') },
            committer: { name: 'Yegor', email: 'yegor3@gmail.com', date: Time.parse('2024-09-13 12:23:25 UTC') },
            message: 'Some text 3',
            tree: { sha: 'bb7277441139739b902a' },
            comment_count: 0
          },
          author: { login: 'yegor258', id: 526_303, type: 'User', site_admin: false },
          committer: { login: 'yegor258', id: 526_303, type: 'User', site_admin: false },
          parents: [{ sha: '18db84d469bb727' }],
          repository: {
            id: 799_177_290, name: 'judges-action', full_name: 'zerocracy/judges-action',
            owner: { login: 'zerocracy', id: 24_234_201, type: 'Organization', site_admin: false }
          }
        }
      ]
    }
  end

  def issue_timeline(_repo, _issue, _options = {})
    [
      {
        event: 'renamed',
        actor: {
          id: 888,
          login: 'torvalds'
        },
        repository: {
          id: name_to_number('yegor256/judges'),
          full_name: 'yegor256/judges'
        },
        rename: {
          from: 'before',
          to: 'after'
        },
        created_at: random_time
      },
      {
        event: 'labeled',
        actor: {
          id: 888,
          login: 'torvalds'
        },
        repository: {
          id: name_to_number('yegor256/judges'),
          full_name: 'yegor256/judges'
        },
        label: {
          name: 'bug'
        },
        created_at: random_time
      },
      {
        node_id: 'ITAE_examplevq862Ga8lzwAAAAQZanzv',
        event: 'issue_type_added',
        actor: {
          id: 526_301,
          login: 'yegor256'
        },
        repository: {
          id: name_to_number('yegor256/judges'),
          full_name: 'yegor256/judges'
        },
        created_at: random_time
      },
      {
        node_id: 'ITCE_examplevq862Ga8lzwAAAAQZbq9S',
        event: 'issue_type_changed',
        actor: {
          id: 526_301,
          login: 'yegor256'
        },
        repository: {
          id: name_to_number('yegor256/judges'),
          full_name: 'yegor256/judges'
        },
        created_at: random_time
      }
    ]
  end

  def repository_events(repo, _options = {})
    [
      {
        id: '123',
        type: 'PushEvent',
        repo: {
          id: name_to_number(repo),
          name: repo,
          url: "https://api.github.com/repos/#{repo}"
        },
        payload: {
          push_id: 42,
          ref: 'refs/heads/master',
          size: 1,
          distinct_size: 0,
          head: 'b7089c51cc2526a0d2619d35379f921d53c72731',
          before: '12d3bff1a55bad50ee2e8f29ade7f1c1e07bb025'
        },
        actor: {
          id: 888,
          login: 'torvalds',
          display_login: 'torvalds'
        },
        created_at: random_time,
        public: true
      },
      {
        id: '124',
        type: 'IssuesEvent',
        repo: {
          id: name_to_number(repo),
          name: repo,
          url: "https://api.github.com/repos/#{repo}"
        },
        payload: {
          action: 'closed',
          issue: {
            number: 42
          }
        },
        actor: {
          id: 888,
          login: 'torvalds',
          display_login: 'torvalds'
        },
        created_at: random_time,
        public: true
      },
      {
        id: '125',
        type: 'IssuesEvent',
        repo: {
          id: name_to_number(repo),
          name: repo,
          url: "https://api.github.com/repos/#{repo}"
        },
        payload: {
          action: 'opened',
          issue: {
            number: 42
          }
        },
        actor: {
          id: 888,
          login: 'torvalds',
          display_login: 'torvalds'
        },
        created_at: random_time,
        public: true
      },
      {
        id: 42,
        created_at: Time.now,
        actor: { id: 42 },
        type: 'PullRequestEvent',
        repo: { id: repo },
        payload: {
          action: 'closed',
          number: 172,
          ref_type: 'tag',
          ref: 'foo',
          pull_request: {
            url: 'https://api.github.com/repos/yegor256/judges/pulls/93',
            id: 1_990_323_142,
            node_id: 'PR_kwDOL6GCO852oevG',
            number: 172,
            state: 'closed',
            locked: false,
            title: '#999 new feature',
            user: {
              login: 'test',
              id: 88_084_038,
              node_id: 'MDQ6VXNlcjE2NDYwMjA=',
              type: 'User',
              site_admin: false
            },
            base: {
              label: 'zerocracy:master',
              ref: 'master',
              user: {
                login: 'zerocracy',
                id: 24_234_201
              },
              repo: {
                id: repo,
                node_id: 'R_kgDOK2_4Aw',
                name: 'baza',
                full_name: 'zerocracy/baza',
                private: false
              }
            },
            head: {
              ref: 'zerocracy/baza',
              sha: '74d0c234967de0f690805c6943e78db42a294c1a'
            },
            merged_at: Time.now,
            comments: 2,
            review_comments: 2,
            commits: 1,
            additions: 3,
            deletions: 3,
            changed_files: 2
          }
        }
      },
      {
        id: 43,
        created_at: Time.now,
        actor: { id: 42 },
        type: 'PullRequestEvent',
        repo: { id: repo },
        payload: {
          action: 'closed',
          number: 172,
          ref_type: 'tag',
          ref: 'foo',
          pull_request: {
            url: 'https://api.github.com/repos/yegor256/judges/pulls/93',
            id: 1_990_323_142,
            node_id: 'PR_kwDOL6GCO852oevG',
            number: 172,
            state: 'closed',
            locked: false,
            title: '#999 new feature',
            user: {
              login: 'test',
              id: 88_084_038,
              node_id: 'MDQ6VXNlcjE2NDYwMjA=',
              type: 'User',
              site_admin: false
            },
            base: {
              label: 'zerocracy:master',
              ref: 'master',
              user: {
                login: 'zerocracy',
                id: 24_234_201
              },
              repo: {
                id: repo,
                node_id: 'R_kgDOK2_4Aw',
                name: 'judges-action',
                full_name: 'zerocracy/judges-action',
                private: false
              }
            },
            head: {
              ref: 'zerocracy/judges-action',
              sha: '74d0c234967de0f690805c6943e78db42a294c1a'
            },
            merged_at: Time.now,
            comments: 2,
            review_comments: 2,
            commits: 1,
            additions: 3,
            deletions: 3,
            changed_files: 2
          }
        }
      }
    ]
  end

  def issue_events(_repo, _number)
    [
      {
        id: 126, actor: { login: 'user', id: 411, type: 'User' },
        event: 'labeled', created_at: Time.parse('2025-05-30 14:41:00 UTC'),
        label: { name: 'bug', color: 'd73a4a' }
      },
      {
        id: 206, actor: { login: 'user', id: 411, type: 'User' },
        event: 'mentioned', created_at: Time.parse('2025-05-30 14:41:10 UTC')
      },
      {
        id: 339, actor: { login: 'user2', id: 422, type: 'User' },
        event: 'subscribed', created_at: Time.parse('2025-05-30 14:41:10 UTC')
      },
      {
        id: 490, actor: { login: 'github-actions[bot]', id: 41_898_282, type: 'Bot' },
        event: 'renamed', created_at: Time.parse('2025-05-30 14:41:30 UTC'),
        rename: { from: 'some title', to: 'some title 2' }
      },
      {
        id: 505, actor: { login: 'user', id: 411, type: 'User' },
        event: 'subscribed', created_at: Time.parse('2025-05-30 16:18:24 UTC')
      },
      {
        id: 608, actor: { login: 'user2', id: 422, type: 'User', test: 123 },
        event: 'assigned', created_at: Time.parse('2025-05-30 17:59:08 UTC'),
        assignee: { login: 'user2', id: 422, type: 'User' },
        assigner: { login: 'user', id: 411, type: 'User' }
      },
      {
        id: 776, actor: { login: 'user2', id: 422, type: 'User' },
        event: 'referenced', commit_id: '4621af032170f43d',
        commit_url: 'https://api.github.com/repos/foo/foo/commits/4621af032170f43d',
        created_at: Time.parse('2025-05-30 19:57:50 UTC')
      }
    ]
  end

  def pull_request_comments(_name, _number)
    [
      {
        pull_request_review_id: 2_227_372_510,
        id: 1_709_082_318,
        path: 'test/baza/test_locks.rb',
        commit_id: 'a9f5f94cf28f29a64d5dd96d0ee23b4174572847',
        original_commit_id: 'e8c6f94274d14ed3cb26fe71467a9c3f229df59c',
        user: {
          login: 'Reviewer',
          id: 2_566_462
        },
        body: 'Most likely, parentheses were missed here.',
        created_at: '2024-08-08T09:41:46Z',
        updated_at: '2024-08-08T09:42:46Z',
        reactions: {
          url: 'https://api.github.com/repos/zerocracy/baza/pulls/comments/1709082318/reactions',
          total_count: 0
        },
        start_line: 'null',
        original_start_line: 'null',
        start_side: 'null',
        line: 'null',
        original_line: 62,
        side: 'RIGHT',
        original_position: 25,
        position: 'null',
        subject_type: 'line'
      },
      {
        pull_request_review_id: 2_227_372_510,
        id: 1_709_082_319,
        path: 'test/baza/test_locks.rb',
        commit_id: 'a9f5f94cf28f29a64d5dd96d0ee23b4174572847',
        original_commit_id: 'e8c6f94274d14ed3cb26fe71467a9c3f229df59c',
        user: {
          login: 'test',
          id: 88_084_038
        },
        body: 'definitely a typo',
        created_at: '2024-08-08T09:42:46Z',
        updated_at: '2024-08-08T09:42:46Z',
        reactions: {
          url: 'https://api.github.com/repos/zerocracy/baza/pulls/comments/1709082319/reactions',
          total_count: 0
        },
        start_line: 'null',
        original_start_line: 'null',
        start_side: 'null',
        line: 'null',
        original_line: 62,
        side: 'RIGHT',
        original_position: 25,
        in_reply_to_id: 1_709_082_318,
        position: 'null',
        subject_type: 'line'
      }
    ]
  end

  def issue_comments(_name, _number)
    [
      {
        pull_request_review_id: 2_227_372_510,
        id: 1_709_082_320,
        path: 'test/baza/test_locks.rb',
        commit_id: 'a9f5f94cf28f29a64d5dd96d0ee23b4174572847',
        original_commit_id: 'e8c6f94274d14ed3cb26fe71467a9c3f229df59c',
        user: {
          login: 'Reviewer',
          id: 2_566_462
        },
        body: 'reviewer comment',
        created_at: '2024-08-08T09:41:46Z',
        updated_at: '2024-08-08T09:42:46Z',
        reactions: {
          url: 'https://api.github.com/repos/zerocracy/baza/pulls/comments/1709082320/reactions',
          total_count: 1
        },
        start_line: 'null',
        original_start_line: 'null',
        start_side: 'null',
        line: 'null',
        original_line: 62,
        side: 'RIGHT',
        original_position: 25,
        position: 'null',
        subject_type: 'line'
      },
      {
        pull_request_review_id: 2_227_372_510,
        id: 1_709_082_321,
        path: 'test/baza/test_locks.rb',
        commit_id: 'a9f5f94cf28f29a64d5dd96d0ee23b4174572847',
        original_commit_id: 'e8c6f94274d14ed3cb26fe71467a9c3f229df59c',
        user: {
          login: 'test',
          id: 88_084_038
        },
        body: 'author comment',
        created_at: '2024-08-08T09:42:46Z',
        updated_at: '2024-08-08T09:42:46Z',
        reactions: {
          url: 'https://api.github.com/repos/zerocracy/baza/pulls/comments/1709082321/reactions',
          total_count: 1
        },
        start_line: 'null',
        original_start_line: 'null',
        start_side: 'null',
        line: 'null',
        original_line: 62,
        side: 'RIGHT',
        original_position: 25,
        in_reply_to_id: 1_709_082_318,
        position: 'null',
        subject_type: 'line'
      }
    ]
  end

  def issue_comment_reactions(_name, _comment)
    [
      {
        id: 248_923_574,
        user: {
          login: 'user',
          id: 8_086_956
        },
        content: 'heart'
      }
    ]
  end

  def pull_request_review_comment_reactions(_name, _comment)
    [
      {
        id: 248_923_574,
        user: {
          login: 'user',
          id: 8_086_956
        },
        content: 'heart'
      }
    ]
  end

  def check_runs_for_ref(repo, sha)
    data = {
      'zerocracy/baza' => {
        total_count: 7,
        check_runs: [
          {
            id: 28_907_016_501,
            name: 'make',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_603,
            name: 'copyrights',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_550,
            name: 'markdown-lint',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_483,
            name: 'pdd',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_433,
            name: 'rake',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_405,
            name: 'shellcheck',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_379,
            name: 'yamllint',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          }
        ]
      },
      'zerocracy/judges-action' => {
        total_count: 7,
        check_runs: [
          {
            id: 28_907_016_501,
            name: 'Codacy Static Code Analysis',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'codacy-production'
            }
          },
          {
            id: 28_906_596_603,
            name: 'copyrights',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_550,
            name: 'markdown-lint',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_483,
            name: 'pdd',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_433,
            name: 'rake',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_405,
            name: 'shellcheck',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          },
          {
            id: 28_906_596_379,
            name: 'yamllint',
            head_sha: sha,
            started_at: '2024-08-18T08:04:44Z',
            completed_at: '2024-08-18T08:20:17Z',
            app: {
              slug: 'github-actions'
            }
          }
        ]
      }
    }
    data.fetch(repo) do
      { total_count: 0, check_runs: [] }
    end
  end

  def workflow_run_job(_repo, job)
    [
      {
        id: 28_907_016_501,
        run_id: 10_438_531_072,
        name: 'make',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 28_906_596_603,
        run_id: 10_438_531_073,
        name: 'copyrights',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 28_906_596_550,
        run_id: 10_438_531_074,
        name: 'markdown-lint',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 28_906_596_483,
        run_id: 10_438_531_075,
        name: 'pdd',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 28_906_596_433,
        run_id: 10_438_531_076,
        name: 'rake',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 28_906_596_405,
        run_id: 10_438_531_077,
        name: 'shellcheck',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 28_906_596_379,
        run_id: 10_438_531_078,
        name: 'yamllint',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      }
    ].find { |json| json[:id] == job } || {
      id: job,
      run_id: 1234,
      name: 'run job',
      started_at: '2024-08-18T08:04:44Z',
      completed_at: '2024-08-18T08:20:17Z'
    }
  end

  def workflow_run(repo, id)
    [
      {
        id: 10_438_531_072,
        event: 'pull_request',
        conclusion: 'success',
        name: 'make',
        started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 10_438_531_073,
        event: 'pull_request',
        conclusion: 'success',
        name: 'copyrights',
        started_at: '2024-08-18T08:04:44Z',
        run_started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 10_438_531_074,
        event: 'pull_request',
        conclusion: 'success',
        name: 'markdown-lint',
        started_at: '2024-08-18T08:04:44Z',
        run_started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 10_438_531_075,
        event: 'pull_request',
        conclusion: 'failure',
        name: 'pdd',
        started_at: '2024-08-18T08:04:44Z',
        run_started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 10_438_531_076,
        event: 'pull_request',
        conclusion: 'success',
        name: 'rake',
        started_at: '2024-08-18T08:04:44Z',
        run_started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 10_438_531_077,
        event: 'commit',
        conclusion: 'success',
        name: 'shellcheck',
        started_at: '2024-08-18T08:04:44Z',
        run_started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      },
      {
        id: 10_438_531_078,
        event: 'pull_request',
        conclusion: 'failure',
        name: 'yamllint',
        started_at: '2024-08-18T08:04:44Z',
        run_started_at: '2024-08-18T08:04:44Z',
        completed_at: '2024-08-18T08:20:17Z'
      }
    ].find { |json| json[:id] == id } || {
      id:,
      name: 'copyrights',
      head_branch: 'master',
      head_sha: '7d34c53e6743944dbf6fc729b1066bcbb3b18443',
      event: 'push',
      status: 'completed',
      conclusion: 'success',
      workflow_id: id,
      created_at: random_time,
      run_started_at: random_time,
      repository: repository(repo)
    }
  end

  def compare(_repo, _start, _end)
    {
      base_commit: {
        sha: '498464613c0b9',
        commit: {
          author: {
            name: 'Yegor Bugayenko', email: 'yegor256@gmail.com', date: Time.parse('2024-09-04 15:23:25 UTC')
          },
          committer: {
            name: 'Yegor Bugayenko', email: 'yegor256@gmail.com', date: Time.parse('2024-09-04 15:23:25 UTC')
          },
          message: 'Some text',
          tree: { sha: '51aee236ba884' },
          comment_count: 0,
          verification: { verified: false, reason: 'unsigned', signature: nil, payload: nil }
        },
        author: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
        committer: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
        parents: [{ sha: '9763dab47b50a12f59c3630690ec2c0f6bdda0b3' }]
      },
      merge_base_commit: {
        sha: '8e4348746638595a7e',
        commit: {
          author: {
            name: 'Yegor Bugayenko', email: 'yegor256@gmail.com', date: Time.parse('2024-08-25 15:57:35 UTC')
          },
          committer: {
            name: 'Yegor Bugayenko', email: 'yegor256@gmail.com', date: Time.parse('2024-08-25 15:57:35 UTC')
          },
          message: 'Some text',
          tree: { sha: '7145fc122e70bf51e1d' },
          comment_count: 0,
          verification: { verified: true, reason: 'valid', signature: '', payload: '' }
        },
        author: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
        committer: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
        parents: [
          { sha: '8c8278efedbd795e70' },
          { sha: '7dfd2e0186113f66f' }
        ]
      },
      status: 'diverged',
      ahead_by: 1,
      behind_by: 30,
      total_commits: 1,
      commits: [
        {
          sha: 'ee04386901692abb',
          commit: {
            author: {
              name: 'Yegor Bugayenko', email: 'yegor256@gmail.com', date: Time.parse('2024-08-25 15:57:35 UTC')
            },
            committer: {
              name: 'Yegor Bugayenko', email: 'yegor256@gmail.com', date: Time.parse('2024-08-25 15:57:35 UTC')
            },
            message: 'Some text',
            tree: { sha: '7a6124a500aed8c92' },
            comment_count: 0,
            verification: { verified: false, reason: 'unsigned', signature: nil, payload: nil }
          },
          author: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
          committer: { login: 'yegor256', id: 526_301, type: 'User', site_admin: false },
          parents: [{ sha: '8e4348746638595a7e' }]
        }
      ],
      files: [
        {
          sha: '9e100c7246c0cc9', filename: 'file.txt', status: 'modified',
          additions: 1, deletions: 1, changes: 2,
          patch: '@@ -24,7 +24,7 @@ text ...'
        },
        {
          sha: 'f97818271059e5455', filename: 'file2.txt', status: 'modified',
          additions: 1, deletions: 1, changes: 2,
          patch: '@@ -25,7 +25,7 @@ text ...'
        },
        {
          sha: '5a957c57d090bfeccb', filename: 'file3.txt', status: 'modified',
          additions: 1, deletions: 1, changes: 2,
          patch: '@@ -27,7 +27,7 @@ text ...'
        }
      ]
    }
  end

  def tree(_repo, _tree_sha, _options = {})
    {
      sha: '492072971ad3c8644a191f62426bd3',
      tree: [
        {
          path: '.github',
          mode: '040000',
          type: 'tree',
          sha: '438682e07e45ccbf9ca58f294a'
        },
        {
          path: '.github/workflows',
          mode: '040000',
          type: 'tree',
          sha: 'dea8a01c236530cc92a63c5774'
        },
        {
          path: '.github/workflows/actionlint.yml',
          mode: '100644',
          type: 'blob',
          sha: 'ffed2deef2383d6f685489b289',
          size: 1671
        },
        {
          path: '.github/workflows/copyrights.yml',
          mode: '100644',
          type: 'blob',
          sha: 'ab8357cfd94e0628676aff34cd',
          size: 1293
        },
        {
          path: '.github/workflows/zerocracy.yml',
          mode: '100644',
          type: 'blob',
          sha: '5c224c7742e5ebeeb176b90605',
          size: 2005
        },
        {
          path: '.gitignore',
          mode: '100644',
          type: 'blob',
          sha: '9383e7111a173b44baa0692775',
          size: 27
        },
        {
          path: '.rubocop.yml',
          mode: '100644',
          type: 'blob',
          sha: 'cb9b62eb1979589daa18142008',
          size: 1963
        },
        {
          path: 'README.md',
          mode: '100644',
          type: 'blob',
          sha: '8011ad43c37edbaf1969417b94',
          size: 4877
        },
        {
          path: 'Rakefile',
          mode: '100644',
          type: 'blob',
          sha: 'a0ac9bf2643d9f5392e1119301',
          size: 1805
        }
      ],
      truncated: false
    }
  end

  def contributors(_repo, _anon = nil, _options = {})
    [
      {
        login: 'yegor256',
        id: 526_301,
        type: 'User',
        contributions: 500
      },
      {
        login: 'renovate[bot]',
        id: 29_139_614,
        type: 'Bot',
        contributions: 320
      },
      {
        login: 'user1',
        id: 2_476_362,
        type: 'User',
        contributions: 120
      },
      {
        login: 'rultor',
        id: 8_086_956,
        type: 'Bot',
        contributions: 87
      },
      {
        login: 'user2',
        id: 5_427_638,
        type: 'User',
        contributions: 49
      },
      {
        login: 'user3',
        id: 2_648_875,
        type: 'User',
        contributions: 10
      },
      {
        login: 'user4',
        id: 7_125_293,
        type: 'User',
        contributions: 1
      }
    ]
  end
end