# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require_relative "../../../base"

require Vagrant.source_root.join("plugins/providers/docker/driver")

describe VagrantPlugins::DockerProvider::Driver do
  let(:cmd_executed) { @cmd }
  let(:cid)          { 'side-1-song-10' }
  let(:execute_result) {
    double("execute_result",
      exit_code: exit_code,
      stderr: stderr,
      stdout: stdout
    )
  }
  let(:exit_code) { 0 }
  let(:stderr) { "" }
  let(:stdout) { "" }

  before do
    allow(Vagrant::Util::Subprocess).to receive(:execute) { |*args|
      if args.last.is_a?(Hash)
        args = args[0, args.size - 1]
      end
      invalid = args.detect { |a| !a.is_a?(String) }
      if invalid
        raise TypeError,
          "Vagrant::Util::Subprocess#execute only accepts signle option Hash and String arguments, received `#{invalid.class}'"
      end
      @cmd = args.join(" ")
    }.and_return(execute_result)
    allow_any_instance_of(Vagrant::Errors::VagrantError).
      to receive(:translate_error) { |*args| args.join(" ") }
  end

  let(:docker_network_struct) {
[
    {
        "Name": "bridge",
        "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d",
        "Created": "2019-03-20T14:10:06.313314662-07:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": nil,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
                "Name": "vagrant-sandbox_docker-1_1553116237",
                "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    },
    {
        "Name": "host",
        "Id": "2a2845e77550e33bf3e97bda8b71477ac7d3ccf78bc9102585fdb6056fb84cbf",
        "Created": "2018-09-28T10:54:08.633543196-07:00",
        "Scope": "local",
        "Driver": "host",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": nil,
            "Config": []
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    },
    {
        "Name": "vagrant_network",
        "Id": "93385d4fd3cf7083a36e62fa72a0ad0a21203d0ddf48409c32b550cd8462b3ba",
        "Created": "2019-03-20T14:10:36.828235585-07:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
                "Name": "vagrant-sandbox_docker-1_1553116237",
                "EndpointID": "9502cd9d37ae6815e3ffeb0bc2de9b84f79e7223e8a1f8f4ccc79459e96c7914",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    },
    {
        "Name": "vagrant_network_172.20.0.0/16",
        "Id": "649f0ab3ef0eef6f2a025c0d0398bd7b9b4d05ec88b0d7bd573b44153d903cfb",
        "Created": "2019-03-20T14:10:37.088885647-07:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.20.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
                "Name": "vagrant-sandbox_docker-1_1553116237",
                "EndpointID": "e19156f8018f283468227fa97c145f4ea0eaba652fb7e977a0c759b1c3ec168a",
                "MacAddress": "02:42:ac:14:80:02",
                "IPv4Address": "172.20.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
].to_json }


  describe '#build' do
    let(:stdout) { "Successfully built other_package\nSuccessfully built 1a2b3c4d" }
    let(:cid) { "1a2b3c4d" }

    it "builds a container with standard docker" do
      container_id = subject.build("/tmp/fakedir")

      expect(container_id).to eq(cid)
    end

    context "using buildkit" do
      let(:stdout) { "writing image sha256:1a2b3c4d 0.0s done" }

      it "builds a container with buildkit docker" do
        container_id = subject.build("/tmp/fakedir")

        expect(container_id).to eq(cid)
      end
    end

    context "using buildkit with old output" do
      let(:stdout) { "writing image sha256:1a2b3c4d done" }

      it "builds a container with buildkit docker (old output)" do
        container_id = subject.build("/tmp/fakedir")

        expect(container_id).to eq(cid)
      end
    end

    context "using podman emulating docker CLI" do
      let(:stdout) { "1a2b3c4d5e6f7g8h9i10j11k12l13m14n16o17p18q19r20s21t22u23v24w25x2" }

      it "builds a container with podman emulating docker CLI" do
        allow(subject).to receive(:podman?).and_return(true)

        container_id = subject.build("/tmp/fakedir")

        expect(container_id).to eq(cid)
      end
    end
  end

  describe '#podman?' do
    context "when docker is used" do
      let(:stdout) { "Docker version 1.8.1, build d12ea79" }

      it 'returns false' do
        expect(subject.podman?).to be false
      end
    end
    context "when podman is used" do
      let(:stdout) { "podman version 1.7.1-dev" }

      it 'returns true' do
        expect(subject.podman?).to be true
      end
    end
  end

  describe '#create' do
    let(:params) { {
      image:      'jimi/hendrix:electric-ladyland',
      cmd:        ['play', 'voodoo-chile'],
      ports:      '8080:80',
      volumes:    '/host/path:guest/path',
      detach:     true,
      links:      [[:janis, 'joplin'], [:janis, 'janis']],
      env:        {key: 'value'},
      name:       cid,
      hostname:   'jimi-hendrix',
      privileged: true
    } }

    before { subject.create(params) }

    it 'runs a detached docker image' do
      expect(cmd_executed).to match(/^docker run .+ -d .+ #{Regexp.escape params[:image]}/)
    end

    it 'sets container name' do
      expect(cmd_executed).to match(/--name #{Regexp.escape params[:name]}/)
    end

    it 'forwards ports' do
      expect(cmd_executed).to match(/-p #{params[:ports]} .+ #{Regexp.escape params[:image]}/)
    end

    it 'shares folders' do
      expect(cmd_executed).to match(/-v #{params[:volumes]} .+ #{Regexp.escape params[:image]}/)
    end

    it 'links containers' do
      params[:links].each do |link|
        expect(cmd_executed).to match(/--link #{link.join(':')} .+ #{Regexp.escape params[:image]}/)
      end
    end

    it 'sets environmental variables' do
      expect(cmd_executed).to match(/-e key=value .+ #{Regexp.escape params[:image]}/)
    end

    it 'is able to run a privileged container' do
      expect(cmd_executed).to match(/--privileged .+ #{Regexp.escape params[:image]}/)
    end

    it 'sets the hostname if specified' do
      expect(cmd_executed).to match(/-h #{params[:hostname]} #{Regexp.escape params[:image]}/)
    end

    it 'executes the provided command' do
      expect(cmd_executed).to match(/#{Regexp.escape params[:image]} #{Regexp.escape params[:cmd].join(' ')}/)
    end
  end

  describe '#create windows' do
    let(:params) { {
      image:      'jimi/hendrix:eletric-ladyland',
      cmd:        ['play', 'voodoo-chile'],
      ports:      '8080:80',
      volumes:    'C:/Users/BobDylan/AllAlong:/The/Watchtower',
      detach:     true,
      links:      [[:janis, 'joplin'], [:janis, 'janis']],
      env:        {key: 'value'},
      name:       cid,
      hostname:   'jimi-hendrix',
      privileged: true
    } }

    let(:translated_path) { "//c/Users/BobDylan/AllAlong:/The/Watchtower" }

    before do
      allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true)
      subject.create(params)
    end

    it 'shares folders' do
      expect(cmd_executed).to match(/-v #{translated_path} .+ #{Regexp.escape params[:image]}/)
    end
  end


  describe '#created?' do
    let(:result) { subject.created?(cid) }

    it 'performs the check on all containers list' do
      subject.created?(cid)
      expect(cmd_executed).to match(/docker ps \-a \-q/)
    end

    context 'when container exists' do
      let(:stdout) { "foo\n#{cid}\nbar" }

      it { expect(result).to be_truthy }
    end

    context 'when container does not exist' do
      let(:stdout) { "foo\n#{cid}extra\nbar" }

      it { expect(result).to be_falsey }
    end
  end

  describe '#pull' do
    it 'should pull images' do
      subject.pull('foo')
      expect(cmd_executed).to match(/docker pull foo/)
    end
  end

  describe '#read_used_ports' do
    let(:all_containers) { ["container1\ncontainer2"] }
    let(:container_info) { {"Name"=>"/container", "State"=>{"Running"=>true}, "HostConfig"=>{"PortBindings"=>{}}} }
    let(:empty_used_ports) { {} }

    context "with existing port forwards" do
      let(:container_info) { {"Name"=>"/container","State"=>{"Running"=>true}, "HostConfig"=>{"PortBindings"=>{"22/tcp"=>[{"HostIp"=>"127.0.0.1","HostPort"=>"2222"}] }}} }
      let(:used_ports_set) { {"2222"=>Set["127.0.0.1"]} }

      context "with active containers" do
        it 'should read all port bindings and return a hash of sets' do
          allow(subject).to receive(:all_containers).and_return(all_containers)
          allow(subject).to receive(:inspect_container).and_return(container_info)

          used_ports = subject.read_used_ports
          expect(used_ports).to eq(used_ports_set)
        end
      end

      context "with inactive containers" do
        let(:container_info) { {"Name"=>"/container", "State"=>{"Running"=>false}, "HostConfig"=>{"PortBindings"=>{"22/tcp"=>[{"HostIp"=>"127.0.0.1","HostPort"=>"2222"}] }}} }

        it 'returns empty' do
          allow(subject).to receive(:all_containers).and_return(all_containers)
          allow(subject).to receive(:inspect_container).and_return(container_info)

          used_ports = subject.read_used_ports
          expect(used_ports).to eq(empty_used_ports)
        end
      end
    end

    it 'returns empty if no ports are already bound' do
      allow(subject).to receive(:all_containers).and_return(all_containers)
      allow(subject).to receive(:inspect_container).and_return(container_info)

      used_ports = subject.read_used_ports
      expect(used_ports).to eq(empty_used_ports)
    end
  end

  describe '#running?' do
    let(:result) { subject.running?(cid) }

    it 'performs the check on the running containers list' do
      subject.running?(cid)
      expect(cmd_executed).to match(/docker ps \-q/)
      expect(cmd_executed).to_not include('-a')
    end

    context 'when container exists' do
      let(:stdout) { "foo\n#{cid}\nbar" }
      it { expect(result).to be_truthy }
    end

    context 'when container does not exist' do
      let(:stdout) { "foo\n#{cid}extra\nbar" }
      it { expect(result).to be_falsey }
    end
  end

  describe '#privileged?' do
    it 'identifies privileged containers' do
      allow(subject).to receive(:inspect_container).and_return({'HostConfig' => {"Privileged" => true}})
      expect(subject).to be_privileged(cid)
    end

    it 'identifies unprivileged containers' do
      allow(subject).to receive(:inspect_container).and_return({'HostConfig' => {"Privileged" => false}})
      expect(subject).to_not be_privileged(cid)
    end
  end

  describe '#start' do
    context 'when container is running' do
      before { allow(subject).to receive(:running?).and_return(true) }

      it 'does not start the container' do
        subject.start(cid)
        expect(cmd_executed).to be_nil
      end
    end

    context 'when container is not running' do
      before { allow(subject).to receive(:running?).and_return(false) }

      it 'starts the container' do
        subject.start(cid)
        expect(cmd_executed).to eq("docker start #{cid}")
      end
    end
  end

  describe '#stop' do
    context 'when container is running' do
      before { allow(subject).to receive(:running?).and_return(true) }

      it 'stops the container' do
        subject.stop(cid, 1)
        expect(cmd_executed).to eq("docker stop -t 1 #{cid}")
      end

      it "stops the container with the set timeout" do
        subject.stop(cid, 5)
        expect(cmd_executed).to eq("docker stop -t 5 #{cid}")
      end
    end

    context 'when container is not running' do
      before { allow(subject).to receive(:running?).and_return(false) }

      it 'does not stop container' do
        subject.stop(cid, 1)
        expect(cmd_executed).to be_nil
      end
    end
  end

  describe '#rm' do
    context 'when container has been created' do
      before { allow(subject).to receive(:created?).and_return(true) }

      it 'removes the container' do
        subject.rm(cid)
        expect(cmd_executed).to eq("docker rm -f -v #{cid}")
      end
    end

    context 'when container has not been created' do
      before { allow(subject).to receive(:created?).and_return(false) }

      it 'does not attempt to remove the container' do
        subject.rm(cid)
        expect(cmd_executed).to be_nil
      end
    end
  end

  describe '#rmi' do
    let(:id) { 'asdg21ew' }

    context 'image exists' do
      it "removes the image" do
        subject.rmi(id)
        expect(cmd_executed).to eq("docker rmi #{id}")
      end
    end

    context 'image is being used by running container' do
      before { allow(subject).to receive(:execute).and_raise("image is being used by running container") }

      it 'does not remove the image' do
        expect(subject.rmi(id)).to eq(false)
        subject.rmi(id)
      end
    end

    context 'image is being used by stopped container' do
      before { allow(subject).to receive(:execute).and_raise("image is being used by stopped container") }

      it 'does not remove the image' do
        expect(subject.rmi(id)).to eq(false)
        subject.rmi(id)
      end
    end

    context 'container is using it' do
      before { allow(subject).to receive(:execute).and_raise("container is using it") }

      it 'does not remove the image' do
        expect(subject.rmi(id)).to eq(false)
        subject.rmi(id)
      end
    end

    context 'image does not exist' do
      before { allow(subject).to receive(:execute).and_raise("No such image") }

      it 'raises an error' do
        expect(subject.rmi(id)).to eq(nil)
        subject.rmi(id)
      end
    end
  end

  describe '#inspect_container' do
    let(:stdout) { '[{"json": "value"}]' }

    it 'inspects the container' do
      subject.inspect_container(cid)
      expect(cmd_executed).to eq("docker inspect #{cid}")
    end

    it 'parses the json output' do
      expect(subject.inspect_container(cid)).to eq('json' => 'value')
    end
  end

  describe '#all_containers' do
    let(:stdout) { "container1\ncontainer2" }

    it 'returns an array of all known containers' do
      expect(subject.all_containers).to eq(['container1', 'container2'])
      expect(cmd_executed).to eq("docker ps -a -q --no-trunc")
    end
  end

  describe '#docker_bridge_ip' do
    context 'from docker network command' do 
    let(:network_struct) {
    [{
        "Name": "bridge",
        "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d",
        "Created": "2019-03-20T14:10:06.313314662-07:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": nil,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
                "Name": "vagrant-sandbox_docker-1_1553116237",
                "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }].to_json
    }
      it "returns the bridge gateway ip" do
        allow(subject).to receive(:inspect_network).and_return(JSON.load(network_struct))
        expect(subject.docker_bridge_ip).to eq('172.17.0.1')
      end
    end

    context 'when falling back to ip' do
      let(:stdout) { " inet 123.456.789.012/16 " }

      it 'returns the bridge ip' do
        expect(subject.docker_bridge_ip).to eq('123.456.789.012')
        expect(cmd_executed).to eq("ip -4 addr show scope global docker0")
      end
    end
  end

  describe '#docker_connect_network' do
    let(:opts) { ["--ip", "172.20.128.2"] }

    it 'connects a network to a container' do
      subject.connect_network("vagrant_network", cid, opts)
      expect(cmd_executed).to eq("docker network connect vagrant_network #{cid} --ip 172.20.128.2")
    end
  end

  describe '#docker_create_network' do
    let(:opts) { ["--subnet", "172.20.0.0/16"] }

    it 'creates a network' do
      subject.create_network("vagrant_network", opts)
      expect(cmd_executed).to eq("docker network create vagrant_network --subnet 172.20.0.0/16")
    end
  end

  describe '#docker_disconnet_network' do
    it 'disconnects a network from a container' do
      subject.disconnect_network("vagrant_network", cid)
      expect(cmd_executed).to eq("docker network disconnect vagrant_network #{cid} --force")
    end
  end

  describe '#docker_inspect_network' do
    it 'gets info about a network' do
      subject.inspect_network("vagrant_network")
      expect(cmd_executed).to eq("docker network inspect vagrant_network")
    end
  end

  describe '#docker_list_network' do
    it 'lists docker networks' do
      subject.list_network()
      expect(cmd_executed).to eq("docker network ls")
    end
  end

  describe '#docker_rm_network' do
    it 'deletes a docker network' do
      subject.rm_network("vagrant_network")
      expect(cmd_executed).to eq("docker network rm vagrant_network")
    end
  end

  describe '#network_defined?' do
    let(:subnet_string) { "172.20.0.0/16" }
    let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }

    it "returns network name if defined" do
      allow(subject).to receive(:list_network_names).and_return(network_names)
      allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))

      network_name = subject.network_defined?(subnet_string)
      expect(network_name).to eq("vagrant_network_172.20.0.0/16")
    end

    it "returns nil name if not defined" do
      allow(subject).to receive(:list_network_names).and_return(network_names)
      allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))

      network_name = subject.network_defined?("120.20.0.0/24")
      expect(network_name).to eq(nil)
    end
  end

  describe '#network_containing_address' do
    let(:address) { "172.20.128.2" }
    let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }

    it "returns the network name if it contains the requested address" do
      allow(subject).to receive(:list_network_names).and_return(network_names)
      allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))

      network_name = subject.network_containing_address(address)
      expect(network_name).to eq("vagrant_network_172.20.0.0/16")
    end

    it "returns nil if no networks contain the requested address" do
      allow(subject).to receive(:list_network_names).and_return(network_names)
      allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))

      network_name = subject.network_containing_address("127.0.0.1")
      expect(network_name).to eq(nil)
    end
  end

  describe '#existing_named_network?' do
    let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }

    it "returns true if the network exists" do
      allow(subject).to receive(:list_network_names).and_return(network_names)

      expect(subject.existing_named_network?("vagrant_network_172.20.0.0/16")).to be_truthy
    end

    it "returns false if the network does not exist" do
      allow(subject).to receive(:list_network_names).and_return(network_names)

      expect(subject.existing_named_network?("vagrant_network_17.0.0/16")).to be_falsey
    end
  end

  describe '#list_network_names' do
    let(:unparsed_network_names) { "vagrant_network_172.20.0.0/16\nbridge\nnull" }
    let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }

    it "lists the network names" do
      allow(subject).to receive(:list_network).with("--format={{.Name}}").
        and_return(unparsed_network_names)

      expect(subject.list_network_names).to eq(network_names)
    end
  end

  describe '#network_used?' do
    let(:network_name) { "vagrant_network_172.20.0.0/16" }
    it "returns nil if no networks" do
      allow(subject).to receive(:inspect_network).with(network_name).and_return(nil)

      expect(subject.network_used?(network_name)).to eq(nil)
    end

    it "returns true if network has containers in use" do
      allow(subject).to receive(:inspect_network).with(network_name).and_return([JSON.load(docker_network_struct).last])

      expect(subject.network_used?(network_name)).to be_truthy
    end

    it "returns false if network has containers in use" do
      allow(subject).to receive(:inspect_network).with("host").and_return([JSON.load(docker_network_struct)[1]])

      expect(subject.network_used?("host")).to be_falsey
    end
  end
end
