# frozen_string_literal: true

RSpec.describe CellManager do
  include ShelloutHelper

  let(:cell_count) { 2 }
  let(:enabled) { true }

  subject { described_class.new }

  before do
    stub_gdk_yaml({
      'cells' => {
        'enabled' => enabled,
        'instance_count' => cell_count
      }
    })
    stub_no_color_env('true')
  end

  shared_examples 'does nothing when cells disabled' do |method, args = []|
    context 'with cells disabled' do
      let(:enabled) { false }

      it 'does nothing' do
        expect_no_gdk_shellout
        expect(GDK::Output).not_to receive(:error)

        expect(subject.method(method).call(*args)).to be(true)
      end
    end

    context 'with no cell instances' do
      let(:cell_count) { 0 }

      it 'does nothing' do
        expect_no_gdk_shellout
        expect(GDK::Output).not_to receive(:error)

        expect(subject.method(method).call(*args)).to be(true)
      end
    end
  end

  shared_examples 'prints error when cell is not set up' do |method, args = []|
    let(:enabled) { true }
    let(:cell_count) { 1 }

    it 'prints an error to run "gdk cells up"', :hide_output do
      expect(Dir).to receive(:exist?).with(cell_dir(2)).and_return(false)
      expect(GDK::Output).to receive(:error).with('Cell 2 doesn’t exist yet, run `gdk cells up` first.')
      expect(subject.method(method).call(*args)).to be(false)
    end
  end

  describe '#up' do
    it_behaves_like 'does nothing when cells disabled', 'up'

    context 'when enabled' do
      let(:cell_2_config) do
        <<~YAML
          port_offset: 12000
          cells:
            enabled: false
            port_offset: 12000
          gitlab_topology_service:
            enabled: false
          gitlab_http_router:
            enabled: false
          gitlab:
            cell:
              id: 2
              database: { skip_sequence_alteration: false }
            rails:
              port: #{GDK.config.port}
              hostname: #{GDK.config.hostname}
              session_store:
                unique_cookie_key_postfix: false
                session_cookie_token_prefix: cell-2
            topology_service:
              address: other address
              enabled: true
              ca_file: #{GDK.config.gitlab.topology_service.ca_file}
              private_key_file: #{GDK.config.gitlab.topology_service.private_key_file}
              certificate_file: #{GDK.config.gitlab.topology_service.certificate_file}
        YAML
      end

      let(:cell_3_config) do
        <<~YAML
          port_offset: 12150
          cells:
            enabled: false
            port_offset: 12150
          gitlab_topology_service:
            enabled: true
          gitlab_http_router:
            enabled: false
          gitlab:
            cell:
              id: 3
              database: { skip_sequence_alteration: false }
            rails:
              port: #{GDK.config.port}
              hostname: #{GDK.config.hostname}
              session_store:
                unique_cookie_key_postfix: false
                session_cookie_token_prefix: cell-3
            topology_service:
              address: #{GDK.config.gitlab.topology_service.address}
              enabled: true
              ca_file: other ca_file
              private_key_file: #{GDK.config.gitlab.topology_service.private_key_file}
              certificate_file: #{GDK.config.gitlab.topology_service.certificate_file}
        YAML
      end

      let(:cell_4_config) do
        <<~YAML
          port_offset: 12300
          cells:
            enabled: false
            port_offset: 12300
          gitlab_topology_service:
            enabled: false
          gitlab_http_router:
            enabled: false
          gitlab:
            cell:
              id: 4
              database: { skip_sequence_alteration: false }
            rails:
              hostname: #{GDK.config.hostname}
              port: #{GDK.config.port}
              session_store:
                unique_cookie_key_postfix: false
                session_cookie_token_prefix: cell-4
            topology_service:
              address: #{GDK.config.gitlab.topology_service.address}
              enabled: true
              ca_file: #{GDK.config.gitlab.topology_service.ca_file}
              private_key_file: #{GDK.config.gitlab.topology_service.private_key_file}
              certificate_file: #{GDK.config.gitlab.topology_service.certificate_file}
        YAML
      end

      before do
        stub_gdk_yaml <<~YAML
          gitlab_http_router:
            enabled: true
          gitlab_topology_service:
            enabled: true
          gitlab:
            rails:
              session_store:
                unique_cookie_key_postfix: false
                session_cookie_token_prefix: cell-1
          cells:
            enabled: true
            instance_count: 3
            instances:
            - config:
                gitlab:
                  topology_service:
                    address: other address
            - config:
                gitlab:
                  topology_service:
                    ca_file: other ca_file
                gitlab_topology_service:
                  enabled: true
            - # cell 4 has no overrides
        YAML

        allow_any_instance_of(GDK::PostgresqlUpgrader).to receive('bin_path_or_fallback').and_return(nil)
        allow(Dir).to receive(:exist?).and_call_original
      end

      context 'when cells exist' do
        it 'writes cell specific configuration', :aggregate_failures, :hide_output do
          stub_cell_exists(2)
          expect_gdk_cells_shellout(2, 'reconfigure')
          expect_gdk_config(2, cell_2_config)

          stub_cell_exists(3)
          expect_gdk_cells_shellout(3, 'reconfigure')
          expect_gdk_config(3, cell_3_config)

          stub_cell_exists(4)
          expect_gdk_cells_shellout(4, 'reconfigure')
          expect_gdk_config(4, cell_4_config)

          subject.up
        end

        context 'when cells do not exist', :hide_output do
          it 'writes cell specific configuration' do
            stub_cell_exists(2, exists: false)
            stub_cell_exists(2, sub_directory: 'gitlab', exists: false)
            expect_gdk_command('git', 'clone', GDK.root.to_s, cell_dir(2))
            expect_gdk_command(*%w[git remote get-url origin], chdir: GDK.root, success: false)
            expect_gdk_command(*%w[git remote set-url origin https://gitlab.com/gitlab-org/gitlab-development-kit.git], chdir: cell_dir(2))
            expect_gdk_cells_shellout(2, "install gitlab_repo=#{GDK.root}/gitlab")
            expect_gdk_cells_shellout(2, 'reconfigure')
            expect_gdk_config(2, cell_2_config)

            stub_cell_exists(3, exists: false)
            stub_cell_exists(3, sub_directory: 'gitlab', exists: false)
            expect_gdk_command('git', 'clone', GDK.root.to_s, cell_dir(3))
            expect_gdk_command(*%w[git remote get-url origin], chdir: GDK.root, stdout: "https://gitlab.com/gitlab-community/gitlab-org/gitlab-development-kit.git")
            expect_gdk_command(*%w[git remote set-url origin https://gitlab.com/gitlab-community/gitlab-org/gitlab-development-kit.git], chdir: cell_dir(3))
            expect_gdk_cells_shellout(3, "install gitlab_repo=#{GDK.root}/gitlab")
            expect_gdk_cells_shellout(3, 'reconfigure')
            expect_gdk_config(3, cell_3_config)

            stub_cell_exists(4, exists: false)
            stub_cell_exists(4, sub_directory: 'gitlab', exists: false)
            expect_gdk_command('git', 'clone', GDK.root.to_s, cell_dir(4))
            expect_gdk_command(*%w[git remote get-url origin], chdir: GDK.root, success: false)
            expect_gdk_command(*%w[git remote set-url origin https://gitlab.com/gitlab-org/gitlab-development-kit.git], chdir: cell_dir(4))
            expect_gdk_cells_shellout(4, "install gitlab_repo=#{GDK.root}/gitlab")
            expect_gdk_cells_shellout(4, 'reconfigure')
            expect_gdk_config(4, cell_4_config)

            subject.up
          end
        end
      end

      def expect_gdk_config(cell_id, expected_yaml)
        cell_gdk_yml = "#{cell_dir(cell_id)}/gdk.yml"
        expected_yaml = YAML.safe_load(expected_yaml)
        expect(File).to receive(:write)
          .with(cell_gdk_yml, be_a_kind_of(String)).twice do |_file, content|
            actual_yaml = YAML.safe_load(content)
            expect(actual_yaml).to match(expected_yaml) if actual_yaml
          end
        expect(File).to receive(:read).with(cell_gdk_yml).and_return('')
      end
    end
  end

  describe '#update' do
    it_behaves_like 'does nothing when cells disabled', 'update'
    it_behaves_like 'prints error when cell is not set up', 'update'

    context 'with cells enabled' do
      it 'updates every cell' do
        stub_cell_exists(2)
        stub_cell_exists(3)

        expect_gdk_cells_shellout(2, 'update')
        expect_gdk_cells_shellout(3, 'update')

        expect { subject.update }.to output(/Updating cell 2\n.*Updating cell 3/m).to_stdout
      end

      context 'when a cell does not exist' do
        it 'prints an error' do
          stub_cell_exists(2)
          stub_cell_exists(3, exists: false)

          expect_gdk_cells_shellout(2, 'update')

          expect { subject.update }.to output(/Updating cell 2\n.*Updating cell 3/m).to_stdout.and output(/Cell 3 doesn’t exist yet, run `gdk cells up` first./).to_stderr
        end
      end

      context 'when the first update fails' do
        it 'skips subsequent updates' do
          stub_cell_exists(2)
          stub_cell_exists(3)

          expect_gdk_cells_shellout(2, 'update', success: false)

          expect { subject.update }.to output(/Updating cell 2\n$/).to_stdout
        end
      end
    end
  end

  describe '#start' do
    it_behaves_like 'does nothing when cells disabled', 'start'
    it_behaves_like 'prints error when cell is not set up', 'start'

    context 'with cells enabled' do
      it 'starts every cell' do
        stub_cell_exists(2)
        stub_cell_exists(3)

        expect_gdk_cells_shellout(2, 'start')
        expect_gdk_cells_shellout(3, 'start')

        expect { subject.start }.not_to output.to_stderr
      end
    end
  end

  describe '#stop' do
    it_behaves_like 'does nothing when cells disabled', 'stop'
    it_behaves_like 'prints error when cell is not set up', 'stop'

    context 'with cells enabled' do
      it 'stops every cell' do
        stub_cell_exists(2)
        stub_cell_exists(3)

        expect_gdk_cells_shellout(2, 'stop')
        expect_gdk_cells_shellout(3, 'stop')

        expect { subject.stop }.not_to output.to_stderr
      end
    end
  end

  describe '#restart' do
    it_behaves_like 'does nothing when cells disabled', 'restart'
    it_behaves_like 'prints error when cell is not set up', 'restart'

    context 'with cells enabled' do
      it 'restarts every cell' do
        stub_cell_exists(2)
        stub_cell_exists(3)

        expect_gdk_cells_shellout(2, 'restart')
        expect_gdk_cells_shellout(3, 'restart')

        expect { subject.restart }.not_to output.to_stderr
      end
    end
  end

  describe '#status' do
    it_behaves_like 'does nothing when cells disabled', 'status'

    context 'with cells enabled' do
      it 'prints the status for every cell' do
        stub_cell_exists(2)
        stub_cell_exists(3)

        expect_gdk_cells_shellout(2, 'status')
        expect_gdk_cells_shellout(3, 'status')

        expect { subject.status }.to output("cell-2\ncell-3\n").to_stdout
      end
    end
  end

  describe '#run_in_cell' do
    context 'with cells disabled' do
      let(:enabled) { false }

      it 'does nothing' do
        expect_no_gdk_shellout
        expect(GDK::Output).not_to receive(:error)

        subject.run_in_cell(2, %w[config list])
      end
    end

    context 'with no cell instances' do
      let(:cell_count) { 0 }

      it 'prints an error' do
        expect_no_gdk_shellout

        expect { subject.run_in_cell(2, %w[config list]) }.to output(
          %r{Cell 2 not found. Check doc/howto/cells.md on how to add local cell instances.}
        ).to_stderr
      end
    end

    context 'with cells enabled' do
      context 'with an unknown cell' do
        it 'prints an error' do
          stub_cell_exists(3, exists: false)

          expect_no_gdk_shellout

          expect { subject.run_in_cell(5, %w[config list]) }.to output(
            /Cell 5 not found. Found: 2, 3./
          ).to_stderr
        end
      end

      context 'when the cell is not set up yet' do
        it 'prints an error' do
          stub_cell_exists(3, exists: false)
          expect_no_gdk_shellout

          expect { subject.run_in_cell(3, %w[config list]) }.to output(
            /Cell 3 doesn’t exist yet, run `gdk cells up` first./
          ).to_stderr
        end
      end

      it 'runs a command in the cell' do
        stub_cell_exists(3)
        expect_gdk_cells_shellout(3, "config list")

        subject.run_in_cell(3, %w[config list])
      end
    end
  end

  describe '#get_config_for' do
    context 'when cell is not configured' do
      it 'raises an error' do
        id = 15

        expect do
          subject.get_config_for(id)
        end.to raise_error(/No config for cell `#{id}` found/)
      end
    end

    context 'when cell is configured' do
      before do
        stub_gdk_yaml({
          'cells' => {
            'enabled' => enabled,
            'instance_count' => 1,
            'instances' => [{
              'config' => {
                'hostname' => 'cell-2.test'
              }
            }]
          }
        })
      end

      it 'returns a config' do
        id = GDK.config.cells.instances.first.id
        config = subject.get_config_for(id)

        expect(config.hostname).to eq('cell-2.test')
      end
    end
  end

  private

  def stub_cell_exists(cell_id, sub_directory: nil, exists: true)
    directory = cell_dir(cell_id)
    directory << "/#{sub_directory}" if sub_directory
    allow(Dir).to receive(:exist?).with(directory).and_return(exists)
  end

  def expect_gdk_cells_shellout(cell_id, arg_str, quiet: false, success: true)
    shellout_double = gdk_shellout_double(success?: success)

    expect_gdk_shellout_command("gdk #{arg_str};", chdir: cell_dir(cell_id)).and_return(shellout_double)
    expect(shellout_double).to receive(:execute).with(display_output: !quiet).and_return(true)
  end

  def expect_gdk_command(*commands, success: true, chdir: nil, stdout: nil)
    shellout_double = gdk_shellout_double(success?: success)

    args = {}
    args[:chdir] = chdir if chdir
    expect_gdk_shellout_command(*commands, **args).and_return(shellout_double)
    expect(shellout_double).to receive(:execute).and_return(success)
    allow(shellout_double).to receive(:read_stdout).and_return(stdout)
  end

  def cell_dir(cell_id)
    "#{GDK.root}/gitlab-cells/cell-#{cell_id}"
  end
end
