require_relative "cli_test_case"

class CliBuildTest < CliTestCase
  test "deliver" do
    Kamal::Cli::Build.any_instance.expects(:push)
    Kamal::Cli::Build.any_instance.expects(:pull)

    run_command("deliver")
  end

  test "push" do
    with_build_directory do |build_directory|
      Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      run_command("push", "--verbose").tap do |output|
        assert_hook_ran "pre-connect", output
        assert_hook_ran "pre-build", output
        assert_match /Cloning repo into build directory/, output
        assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
        assert_match /docker --version && docker buildx version/, output
        assert_match /docker buildx build --output=type=registry --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. 2>&1 as .*@localhost/, output
      end
    end
  end

  test "push with remote builder checks both the builder and the remote context" do
    with_build_directory do |build_directory|
      Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      run_command("push", "--verbose", fixture: :with_remote_builder).tap do |output|
        assert_no_match "Running docker login -u [REDACTED] -p [REDACTED] as ", output
        assert_match "docker buildx inspect kamal-remote-ssh---app-1-1-1-5 | grep -q Endpoint:.*kamal-remote-ssh---app-1-1-1-5-context && docker context inspect kamal-remote-ssh---app-1-1-1-5-context --format '{{.Endpoints.docker.Host}}' | grep -xq ssh://app@1.1.1.5 || (echo no compatible builder && exit 1)", output
        assert_match "Command: ( export BUILDKIT_NO_CLIENT_TOKEN=\"1\" ; docker buildx build --output=type=registry --platform linux/arm64 --builder kamal-remote-ssh---app-1-1-1-5 -t dhh/app:999 -t dhh/app:latest --label service=\"app\" --file Dockerfile . 2>&1 )", output
      end
    end
  end

  test "push --output=docker" do
    with_build_directory do |build_directory|
      Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      run_command("push", "--output=docker", "--verbose").tap do |output|
        assert_hook_ran "pre-build", output
        assert_match /Cloning repo into build directory/, output
        assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
        assert_match /docker --version && docker buildx version/, output
        assert_match /docker buildx build --output=type=docker --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. 2>&1 as .*@localhost/, output
      end
    end
  end

  test "push resetting clone" do
    with_build_directory do |build_directory|
      stub_setup

      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
      SSHKit::Backend::Abstract.any_instance.stubs(:execute).with { |*args| args[0..1] == [ :docker, :login ] }

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules")
        .raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
        .then
        .returns(true)
      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :remote, "set-url", :origin, Dir.pwd)
      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin)
      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision)
      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :build, "--output=type=registry", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", "2>&1", env: {})

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      run_command("push", "--verbose").tap do |output|
        assert_match /Cloning repo into build directory/, output
        assert_match /Resetting local clone/, output
      end
    end
  end

  test "push without clone" do
    Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

    run_command("push", "--verbose", fixture: :without_clone).tap do |output|
      assert_no_match /Cloning repo into build directory/, output
      assert_hook_ran "pre-build", output
      assert_match /docker --version && docker buildx version/, output
      assert_match /docker buildx build --output=type=registry --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . 2>&1 as .*@localhost/, output
    end
  end

  test "push with no-cache" do
    Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

    run_command("push", "--no-cache", "--verbose", fixture: :without_clone).tap do |output|
      assert_hook_ran "pre-build", output
      assert_match /docker --version && docker buildx version/, output
      assert_match /docker buildx build --output=type=registry --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile --no-cache . 2>&1 as .*@localhost/, output
    end
  end

  test "push with corrupt clone" do
    with_build_directory do |build_directory|
      stub_setup

      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
      SSHKit::Backend::Abstract.any_instance.stubs(:execute).with { |*args| args[0..1] == [ :docker, :login ] }

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules")
        .raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
        .then
        .returns(true)
        .twice

      SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :remote, "set-url", :origin, Dir.pwd)
        .raises(SSHKit::Command::Failed.new("fatal: not a git repository"))

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      Dir.stubs(:chdir)

      run_command("push", "--verbose") do |output|
        assert_match /Cloning repo into build directory `#{build_directory}`\.\.\..*Cloning repo into build directory `#{build_directory}`\.\.\./, output
        assert_match "Resetting local clone as `#{build_directory}` already exists...", output
        assert_match "Error preparing clone: Failed to clone repo: fatal: not a git repository, deleting and retrying...", output
      end
    end
  end

  test "push without builder for local registry" do
    with_build_directory do |build_directory|
      stub_setup

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, "--version", "&&", :docker, :buildx, "version")

      SSHKit::Backend::Abstract.any_instance.stubs(:execute)
        .with { |*args| args[0..1] == [ :docker, :login ] }

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
      .with(:docker, :start, "kamal-docker-registry", "||", :docker, :run, "--detach", "-p", "127.0.0.1:5000:5000", "--name", "kamal-docker-registry", "registry:3")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :rm, "kamal-local-registry-docker-container")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :create, "--name", "kamal-local-registry-docker-container", "--driver=docker-container --driver-opt network=host")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :inspect, "kamal-local-registry-docker-container")
        .raises(SSHKit::Command::Failed.new("no builder"))

      SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.to_s.start_with?("git") }

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :build, "--output=type=registry", "--platform", "linux/amd64", "--builder", "kamal-local-registry-docker-container", "-t", "localhost:5000/dhh/app:999", "-t", "localhost:5000/dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", "2>&1", env: {})

      run_command("push", fixture: :with_local_registry_and_accessories).tap do |output|
        assert_match /WARN Missing compatible builder, so creating a new one first/, output
      end
    end
  end

  test "push without builder" do
    with_build_directory do |build_directory|
      stub_setup

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, "--version", "&&", :docker, :buildx, "version")

      SSHKit::Backend::Abstract.any_instance.stubs(:execute)
        .with { |*args| args[0..1] == [ :docker, :login ] }

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :rm, "kamal-local-docker-container")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :inspect, "kamal-local-docker-container")
        .raises(SSHKit::Command::Failed.new("no builder"))

      SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") }

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :"rev-parse", :HEAD)
        .returns(Kamal::Git.revision)

      SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
        .with(:git, "-C", anything, :status, "--porcelain")
        .returns("")

      SSHKit::Backend::Abstract.any_instance.expects(:execute)
        .with(:docker, :buildx, :build, "--output=type=registry", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", "2>&1", env: {})

      run_command("push").tap do |output|
        assert_match /WARN Missing compatible builder, so creating a new one first/, output
      end
    end
  end

  test "push with no buildx plugin" do
    stub_setup
    SSHKit::Backend::Abstract.any_instance.stubs(:execute)
      .with(:docker, "--version", "&&", :docker, :buildx, "version")
      .raises(SSHKit::Command::Failed.new("no buildx"))

    Kamal::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false)
    assert_raises(Kamal::Cli::DependencyError) { run_command("push") }
  end

  test "push pre-build hook failure" do
    fail_hook("pre-build")

    error = assert_raises(Kamal::Cli::HookError) { run_command("push") }
    assert_equal "Hook `pre-build` failed:\nfailed", error.message

    assert @executions.none? { |args| args[0..2] == [ :docker, :build ] }
  end

  test "pull" do
    run_command("pull").tap do |output|
      assert_match /docker info --format '{{index .RegistryConfig.Mirrors 0}}'/, output
      assert_match /docker image rm --force dhh\/app:999/, output
      assert_match /docker pull dhh\/app:999/, output
      assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
    end
  end

  test "pull with mirror" do
    SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
      .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
      .returns("registry-mirror.example.com")
      .at_least_once

    run_command("pull").tap do |output|
      assert_match /Pulling image on 1\.1\.1\.\d to seed the mirror\.\.\./, output
      assert_match "Pulling image on remaining hosts...", output
      assert_equal 4, output.scan(/docker pull dhh\/app:999/).size, output
      assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
    end
  end

  test "pull with mirrors" do
    SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
      .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
      .returns("registry-mirror.example.com", "registry-mirror2.example.com")
      .at_least_once

    run_command("pull").tap do |output|
      assert_match /Pulling image on 1\.1\.1\.\d, 1\.1\.1\.\d to seed the mirrors\.\.\./, output
      assert_match "Pulling image on remaining hosts...", output
      assert_equal 4, output.scan(/docker pull dhh\/app:999/).size, output
      assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
    end
  end

  test "create" do
    run_command("create").tap do |output|
      assert_match /docker buildx create --name kamal-local-docker-container --driver=docker-container/, output
    end
  end

  test "create remote" do
    run_command("create", fixture: :with_remote_builder).tap do |output|
      assert_match "Running /usr/bin/env true on 1.1.1.5", output
      assert_match "docker context create kamal-remote-ssh---app-1-1-1-5-context --description 'kamal-remote-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
      assert_match "docker buildx create --name kamal-remote-ssh---app-1-1-1-5 kamal-remote-ssh---app-1-1-1-5-context", output
    end
  end

  test "create remote with custom ports" do
    run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output|
      assert_match "Running /usr/bin/env true on 1.1.1.5", output
      assert_match "docker context create kamal-remote-ssh---app-1-1-1-5-2122-context --description 'kamal-remote-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output
      assert_match "docker buildx create --name kamal-remote-ssh---app-1-1-1-5-2122 kamal-remote-ssh---app-1-1-1-5-2122-context", output
    end
  end

  test "create hybrid" do
    run_command("create", fixture: :with_hybrid_builder).tap do |output|
      assert_match "Running /usr/bin/env true on 1.1.1.5", output
      assert_match "docker buildx create --platform linux/#{Kamal::Utils.docker_arch} --name kamal-hybrid-docker-container-ssh---app-1-1-1-5 --driver=docker-container", output
      assert_match "docker context create kamal-hybrid-docker-container-ssh---app-1-1-1-5-context --description 'kamal-hybrid-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
      assert_match "docker buildx create --platform linux/#{Kamal::Utils.docker_arch == "amd64" ? "arm64" : "amd64"} --append --name kamal-hybrid-docker-container-ssh---app-1-1-1-5 kamal-hybrid-docker-container-ssh---app-1-1-1-5-context", output
    end
  end

  test "create cloud" do
    run_command("create", fixture: :with_cloud_builder).tap do |output|
      assert_match /docker buildx create --driver cloud example_org\/cloud_builder/, output
    end
  end

  test "create with error" do
    stub_setup
    SSHKit::Backend::Abstract.any_instance.stubs(:execute)
      .with { |arg| arg == :docker }
      .raises(SSHKit::Command::Failed.new("stderr=error"))

    run_command("create").tap do |output|
      assert_match /Couldn't create remote builder: error/, output
    end
  end

  test "remove" do
    run_command("remove").tap do |output|
      assert_match /docker buildx rm kamal-local/, output
    end
  end

  test "remove cloud" do
    run_command("remove", fixture: :with_cloud_builder).tap do |output|
      assert_match /docker buildx rm cloud-example_org-cloud_builder/, output
    end
  end

  test "details" do
    SSHKit::Backend::Abstract.any_instance.stubs(:capture)
      .with(:docker, :context, :ls, "&&", :docker, :buildx, :ls)
      .returns("docker builder info")

    run_command("details").tap do |output|
      assert_match /Builder: local/, output
      assert_match /docker builder info/, output
    end
  end

  test "dev" do
    with_build_directory do |build_directory|
      Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

      run_command("dev", "--verbose").tap do |output|
        assert_no_match(/Cloning repo into build directory/, output)
        assert_match(/docker --version && docker buildx version/, output)
        assert_match(/docker buildx build --output=type=docker --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile \. 2>&1 as .*@localhost/, output)
      end
    end
  end

  test "dev --output=local" do
    with_build_directory do |build_directory|
      Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

      run_command("dev", "--output=local", "--verbose").tap do |output|
        assert_no_match(/Cloning repo into build directory/, output)
        assert_match(/docker --version && docker buildx version/, output)
        assert_match(/docker buildx build --output=type=local --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile \. 2>&1 as .*@localhost/, output)
      end
    end
  end

  test "dev with no-cache" do
    with_build_directory do |build_directory|
      Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)

      run_command("dev", "--no-cache", "--verbose").tap do |output|
        assert_no_match(/Cloning repo into build directory/, output)
        assert_match(/docker --version && docker buildx version/, output)
        assert_match(/docker buildx build --output=type=docker --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile --no-cache \. 2>&1 as .*@localhost/, output)
      end
    end
  end

  private
    def run_command(*command, fixture: :with_accessories)
      stdouted { stderred { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } }
    end

    def stub_dependency_checks
      SSHKit::Backend::Abstract.any_instance.stubs(:execute)
        .with(:docker, "--version", "&&", :docker, :buildx, "version")
      SSHKit::Backend::Abstract.any_instance.stubs(:execute)
        .with { |*args| args[0..1] == [ :docker, :buildx ] }
    end
end
