/*-
 * -\-\-
 * Helios Client
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * -/-/-
 */

package com.spotify.helios.common.descriptors;

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.spotify.helios.common.descriptors.Descriptor.parse;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
import com.spotify.helios.common.Hash;
import com.spotify.helios.common.Json;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;

public class JobTest {

  private Map<String, Object> map(final Object... objects) {
    final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
    checkArgument(objects.length % 2 == 0);
    for (int i = 0; i < objects.length; i += 2) {
      builder.put((String) objects[i], objects[i + 1]);
    }
    return builder.build();
  }

  @Test
  public void testNormalizedExcludesEmptyStrings() throws Exception {
    final Job j = Job.newBuilder().setName("x").setImage("x").setVersion("x")
        .setRegistrationDomain("").build();
    assertFalse(Json.asNormalizedString(j).contains("registrationDomain"));
  }

  @Test
  public void verifyBuilder() throws Exception {
    final Job.Builder builder = Job.newBuilder();

    // Input to setXXX
    final String setName = "set_name";
    final String setVersion = "set_version";
    final String setImage = "set_image";
    final String setHostname = "set_hostname";
    final List<String> setCommand = asList("set", "command");
    final Map<String, String> setEnv = ImmutableMap.of("set", "env");
    final Map<String, PortMapping> setPorts = ImmutableMap.of("set_ports", PortMapping.of(1234));
    final ImmutableMap.Builder<String, ServicePortParameters> setServicePortsBuilder =
        ImmutableMap.builder();
    setServicePortsBuilder.put("set_ports1", new ServicePortParameters(
        ImmutableList.of("tag1", "tag2")));
    setServicePortsBuilder.put("set_ports2", new ServicePortParameters(
        ImmutableList.of("tag3", "tag4")));
    final ServicePorts setServicePorts = new ServicePorts(setServicePortsBuilder.build());
    final Map<ServiceEndpoint, ServicePorts> setRegistration = ImmutableMap.of(
        ServiceEndpoint.of("set_service", "set_proto"), setServicePorts);
    final Integer setGracePeriod = 120;
    final Map<String, String> setVolumes = ImmutableMap.of("/set", "/volume");
    final Date setExpires = new Date();
    final String setRegistrationDomain = "my.domain";
    final String setCreatingUser = "username";
    final Resources setResources = new Resources(10485760L, 10485761L, 4L, "1");
    final HealthCheck setHealthCheck = HealthCheck.newHttpHealthCheck()
        .setPath("/healthcheck")
        .setPort("set_ports")
        .build();
    final List<String> setSecurityOpt = Lists.newArrayList("label:user:dxia", "apparmor:foo");
    final String setNetworkMode = "host";
    final Map<String, String> setMetadata = ImmutableMap.of("set_metadata_key", "set_metadata_val");
    final Set<String> setAddCapabilities = ImmutableSet.of("set_cap_add1", "set_cap_add2");
    final Set<String> setDropCapabilities = ImmutableSet.of("set_cap_drop1", "set_cap_drop2");
    final Map<String, String> setLabels = ImmutableMap.of("set_label_key", "set_label_val");

    // Input to addXXX
    final Map<String, String> addEnv = ImmutableMap.of("add", "env");
    final Map<String, PortMapping> addPorts = ImmutableMap.of("add_ports", PortMapping.of(4711));
    final ImmutableMap.Builder<String, ServicePortParameters> addServicePortsBuilder =
        ImmutableMap.builder();
    addServicePortsBuilder.put("add_ports1", new ServicePortParameters(
        ImmutableList.of("tag1", "tag2")));
    addServicePortsBuilder.put("add_ports2", new ServicePortParameters(
        ImmutableList.of("tag3", "tag4")));
    final ServicePorts addServicePorts = new ServicePorts(addServicePortsBuilder.build());
    final Map<ServiceEndpoint, ServicePorts> addRegistration = ImmutableMap.of(
        ServiceEndpoint.of("add_service", "add_proto"), addServicePorts);
    final Map<String, String> addVolumes = ImmutableMap.of("/add", "/volume");
    final Map<String, String> addMetadata = ImmutableMap.of("add_metadata_key", "add_metadata_val");
    final Map<String, String> addLabels = ImmutableMap.of("add_labels_key", "add_labels_val");

    // Expected output from getXXX
    final String expectedName = setName;
    final String expectedVersion = setVersion;
    final String expectedImage = setImage;
    final String expectedHostname = setHostname;
    final List<String> expectedCommand = setCommand;
    final Map<String, String> expectedEnv = concat(setEnv, addEnv);
    final Map<String, PortMapping> expectedPorts = concat(setPorts, addPorts);
    final Map<ServiceEndpoint, ServicePorts> expectedRegistration =
        concat(setRegistration, addRegistration);
    final Integer expectedGracePeriod = setGracePeriod;
    final Map<String, String> expectedVolumes = concat(setVolumes, addVolumes);
    final Date expectedExpires = setExpires;
    final String expectedRegistrationDomain = setRegistrationDomain;
    final String expectedCreatingUser = setCreatingUser;
    final Resources expectedResources = setResources;
    final HealthCheck expectedHealthCheck = setHealthCheck;
    final List<String> expectedSecurityOpt = setSecurityOpt;
    final String expectedNetworkMode = setNetworkMode;
    final Map<String, String> expectedMetadata = concat(setMetadata, addMetadata);
    final Set<String> expectedAddCapabilities = setAddCapabilities;
    final Set<String> expectedDropCapabilities = setDropCapabilities;
    final Map<String, String> expectedLabels = concat(setLabels, addLabels);

    // Check setXXX methods
    builder.setName(setName);
    builder.setVersion(setVersion);
    builder.setImage(setImage);
    builder.setHostname(setHostname);
    builder.setCommand(setCommand);
    builder.setEnv(setEnv);
    builder.setPorts(setPorts);
    builder.setRegistration(setRegistration);
    builder.setGracePeriod(setGracePeriod);
    builder.setVolumes(setVolumes);
    builder.setExpires(setExpires);
    builder.setRegistrationDomain(setRegistrationDomain);
    builder.setCreatingUser(setCreatingUser);
    builder.setResources(setResources);
    builder.setHealthCheck(setHealthCheck);
    builder.setSecurityOpt(setSecurityOpt);
    builder.setNetworkMode(setNetworkMode);
    builder.setMetadata(setMetadata);
    builder.setAddCapabilities(setAddCapabilities);
    builder.setDropCapabilities(setDropCapabilities);
    builder.setLabels(setLabels);

    // Check addXXX methods
    for (final Map.Entry<String, String> entry : addEnv.entrySet()) {
      builder.addEnv(entry.getKey(), entry.getValue());
    }
    for (final Map.Entry<String, PortMapping> entry : addPorts.entrySet()) {
      builder.addPort(entry.getKey(), entry.getValue());
    }
    for (final Map.Entry<ServiceEndpoint, ServicePorts> entry : addRegistration.entrySet()) {
      builder.addRegistration(entry.getKey(), entry.getValue());
    }
    for (final Map.Entry<String, String> entry : addVolumes.entrySet()) {
      builder.addVolume(entry.getKey(), entry.getValue());
    }
    for (final Map.Entry<String, String> entry : addMetadata.entrySet()) {
      builder.addMetadata(entry.getKey(), entry.getValue());
    }
    for (final Map.Entry<String, String> entry : addLabels.entrySet()) {
      builder.addLabels(entry.getKey(), entry.getValue());
    }

    assertEquals("name", expectedName, builder.getName());
    assertEquals("version", expectedVersion, builder.getVersion());
    assertEquals("image", expectedImage, builder.getImage());
    assertEquals("hostname", expectedHostname, builder.getHostname());
    assertEquals("command", expectedCommand, builder.getCommand());
    assertEquals("env", expectedEnv, builder.getEnv());
    assertEquals("ports", expectedPorts, builder.getPorts());
    assertEquals("registration", expectedRegistration, builder.getRegistration());
    assertEquals("gracePeriod", expectedGracePeriod, builder.getGracePeriod());
    assertEquals("volumes", expectedVolumes, builder.getVolumes());
    assertEquals("expires", expectedExpires, builder.getExpires());
    assertEquals("registrationDomain", expectedRegistrationDomain, builder.getRegistrationDomain());
    assertEquals("creatingUser", expectedCreatingUser, builder.getCreatingUser());
    assertEquals("resources", expectedResources, builder.getResources());
    assertEquals("healthCheck", expectedHealthCheck, builder.getHealthCheck());
    assertEquals("securityOpt", expectedSecurityOpt, builder.getSecurityOpt());
    assertEquals("networkMode", expectedNetworkMode, builder.getNetworkMode());
    assertEquals("metadata", expectedMetadata, builder.getMetadata());
    assertEquals("addCapabilities", expectedAddCapabilities, builder.getAddCapabilities());
    assertEquals("dropCapabilities", expectedDropCapabilities,
        builder.getDropCapabilities());
    assertEquals("labels", expectedLabels, builder.getLabels());

    // Check final output
    final Job job = builder.build();
    assertEquals("name", expectedName, job.getId().getName());
    assertEquals("version", expectedVersion, job.getId().getVersion());
    assertEquals("image", expectedImage, job.getImage());
    assertEquals("hostname", expectedHostname, job.getHostname());
    assertEquals("command", expectedCommand, job.getCommand());
    assertEquals("env", expectedEnv, job.getEnv());
    assertEquals("ports", expectedPorts, job.getPorts());
    assertEquals("registration", expectedRegistration, job.getRegistration());
    assertEquals("gracePeriod", expectedGracePeriod, job.getGracePeriod());
    assertEquals("volumes", expectedVolumes, job.getVolumes());
    assertEquals("expires", expectedExpires, job.getExpires());
    assertEquals("registrationDomain", expectedRegistrationDomain, job.getRegistrationDomain());
    assertEquals("creatingUser", expectedCreatingUser, job.getCreatingUser());
    assertEquals("resources", expectedResources, job.getResources());
    assertEquals("healthCheck", expectedHealthCheck, job.getHealthCheck());
    assertEquals("securityOpt", expectedSecurityOpt, job.getSecurityOpt());
    assertEquals("networkMode", expectedNetworkMode, job.getNetworkMode());
    assertEquals("metadata", expectedMetadata, job.getMetadata());
    assertEquals("addCapabilities", expectedAddCapabilities, job.getAddCapabilities());
    assertEquals("dropCapabilities", expectedDropCapabilities, job.getDropCapabilities());
    assertEquals("labels", expectedLabels, job.getLabels());

    // Check toBuilder
    final Job.Builder rebuilder = job.toBuilder();
    assertEquals("name", expectedName, rebuilder.getName());
    assertEquals("version", expectedVersion, rebuilder.getVersion());
    assertEquals("image", expectedImage, rebuilder.getImage());
    assertEquals("hostname", expectedHostname, rebuilder.getHostname());
    assertEquals("command", expectedCommand, rebuilder.getCommand());
    assertEquals("env", expectedEnv, rebuilder.getEnv());
    assertEquals("ports", expectedPorts, rebuilder.getPorts());
    assertEquals("registration", expectedRegistration, rebuilder.getRegistration());
    assertEquals("gracePeriod", expectedGracePeriod, rebuilder.getGracePeriod());
    assertEquals("volumes", expectedVolumes, rebuilder.getVolumes());
    assertEquals("expires", expectedExpires, rebuilder.getExpires());
    assertEquals("registrationDomain", expectedRegistrationDomain,
        rebuilder.getRegistrationDomain());
    assertEquals("creatingUser", expectedCreatingUser, rebuilder.getCreatingUser());
    assertEquals("resources", expectedResources, rebuilder.getResources());
    assertEquals("healthCheck", expectedHealthCheck, rebuilder.getHealthCheck());
    assertEquals("securityOpt", expectedSecurityOpt, rebuilder.getSecurityOpt());
    assertEquals("networkMode", expectedNetworkMode, rebuilder.getNetworkMode());
    assertEquals("metadata", expectedMetadata, rebuilder.getMetadata());
    assertEquals("addCapabilities", expectedAddCapabilities, rebuilder.getAddCapabilities());
    assertEquals("dropCapabilities", expectedDropCapabilities,
        rebuilder.getDropCapabilities());
    assertEquals("labels", expectedLabels, rebuilder.getLabels());

    // Check clone
    final Job.Builder cloned = builder.clone();
    assertEquals("name", expectedName, cloned.getName());
    assertEquals("version", expectedVersion, cloned.getVersion());
    assertEquals("image", expectedImage, cloned.getImage());
    assertEquals("hostname", expectedHostname, cloned.getHostname());
    assertEquals("command", expectedCommand, cloned.getCommand());
    assertEquals("env", expectedEnv, cloned.getEnv());
    assertEquals("ports", expectedPorts, cloned.getPorts());
    assertEquals("registration", expectedRegistration, cloned.getRegistration());
    assertEquals("gracePeriod", expectedGracePeriod, cloned.getGracePeriod());
    assertEquals("volumes", expectedVolumes, cloned.getVolumes());
    assertEquals("expires", expectedExpires, cloned.getExpires());
    assertEquals("registrationDomain", expectedRegistrationDomain,
        cloned.getRegistrationDomain());
    assertEquals("creatingUser", expectedCreatingUser, cloned.getCreatingUser());
    assertEquals("resources", expectedResources, cloned.getResources());
    assertEquals("healthCheck", expectedHealthCheck, cloned.getHealthCheck());
    assertEquals("securityOpt", expectedSecurityOpt, cloned.getSecurityOpt());
    assertEquals("networkMode", expectedNetworkMode, cloned.getNetworkMode());
    assertEquals("metadata", expectedMetadata, cloned.getMetadata());
    assertEquals("addCapabilities", expectedAddCapabilities, cloned.getAddCapabilities());
    assertEquals("dropCapabilities", expectedDropCapabilities,
        cloned.getDropCapabilities());
    assertEquals("labels", expectedLabels, cloned.getLabels());

    final Job clonedJob = cloned.build();
    assertEquals("name", expectedName, clonedJob.getId().getName());
    assertEquals("version", expectedVersion, clonedJob.getId().getVersion());
    assertEquals("image", expectedImage, clonedJob.getImage());
    assertEquals("hostname", expectedHostname, clonedJob.getHostname());
    assertEquals("command", expectedCommand, clonedJob.getCommand());
    assertEquals("env", expectedEnv, clonedJob.getEnv());
    assertEquals("ports", expectedPorts, clonedJob.getPorts());
    assertEquals("registration", expectedRegistration, clonedJob.getRegistration());
    assertEquals("gracePeriod", expectedGracePeriod, clonedJob.getGracePeriod());
    assertEquals("volumes", expectedVolumes, clonedJob.getVolumes());
    assertEquals("expires", expectedExpires, clonedJob.getExpires());
    assertEquals("registrationDomain", expectedRegistrationDomain,
        clonedJob.getRegistrationDomain());
    assertEquals("creatingUser", expectedCreatingUser, clonedJob.getCreatingUser());
    assertEquals("resources", expectedResources, clonedJob.getResources());
    assertEquals("healthCheck", expectedHealthCheck, clonedJob.getHealthCheck());
    assertEquals("securityOpt", expectedSecurityOpt, clonedJob.getSecurityOpt());
    assertEquals("networkMode", expectedNetworkMode, clonedJob.getNetworkMode());
    assertEquals("metadata", expectedMetadata, clonedJob.getMetadata());
    assertEquals("addCapabilities", expectedAddCapabilities, clonedJob.getAddCapabilities());
    assertEquals("dropCapabilities", expectedDropCapabilities,
        clonedJob.getDropCapabilities());
    assertEquals("labels", expectedLabels, clonedJob.getLabels());
  }

  @SafeVarargs
  private final <K, V> Map<K, V> concat(final Map<K, V>... maps) {
    final ImmutableMap.Builder<K, V> b = ImmutableMap.builder();
    for (final Map<K, V> map : maps) {
      b.putAll(map);
    }
    return b.build();
  }

  /** Verify the Builder allows calling addFoo() before setFoo() for collection types. */
  @Test
  public void testBuilderAddBeforeSet() throws Exception {
    final Job job = Job.newBuilder()
        .addEnv("env", "var")
        .addMetadata("meta", "data")
        .addPort("http", PortMapping.of(80, 8000))
        .addRegistration(ServiceEndpoint.of("foo", "http"), ServicePorts.of("http"))
        .addVolume("/foo", "/bar")
        .build();

    assertThat(job.getEnv(), hasEntry("env", "var"));
    assertThat(job.getMetadata(), hasEntry("meta", "data"));
    assertThat(job.getPorts(), hasEntry("http", PortMapping.of(80, 8000)));
    assertThat(job.getRegistration(),
        hasEntry(ServiceEndpoint.of("foo", "http"), ServicePorts.of("http")));
    assertThat(job.getVolumes(), hasEntry("/foo", "/bar"));
  }

  @Test
  public void verifySha1Id() throws IOException {
    final Map<String, Object> expectedConfig = map("command", asList("foo", "bar"),
        "image", "foobar:4711",
        "name", "foozbarz",
        "version", "17");

    final String expectedInput = "foozbarz:17:" + hex(Json.sha1digest(expectedConfig));
    final String expectedDigest = hex(Hash.sha1digest(expectedInput.getBytes(UTF_8)));
    final JobId expectedId = JobId.fromString("foozbarz:17:" + expectedDigest);

    final Job job = Job.newBuilder()
        .setCommand(asList("foo", "bar"))
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17")
        .build();

    assertEquals(expectedId, job.getId());
  }

  @Test
  public void verifySha1IdWithEnv() throws IOException {
    final Map<String, String> env = ImmutableMap.of("FOO", "BAR");
    final Map<String, Object> expectedConfig = map("command", asList("foo", "bar"),
        "image", "foobar:4711",
        "name", "foozbarz",
        "version", "17",
        "env", env);

    final String expectedInput = "foozbarz:17:" + hex(Json.sha1digest(expectedConfig));
    final String expectedDigest = hex(Hash.sha1digest(expectedInput.getBytes(UTF_8)));
    final JobId expectedId = JobId.fromString("foozbarz:17:" + expectedDigest);

    final Job job = Job.newBuilder()
        .setCommand(asList("foo", "bar"))
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17")
        .setEnv(env)
        .build();

    assertEquals(expectedId, job.getId());
  }

  private String hex(final byte[] bytes) {
    return BaseEncoding.base16().lowerCase().encode(bytes);
  }

  @Test
  public void verifyCanParseJobWithUnknownFields() throws Exception {
    final Job job = Job.newBuilder()
        .setCommand(asList("foo", "bar"))
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17")
        .build();

    final String jobJson = job.toJsonString();

    final ObjectMapper objectMapper = new ObjectMapper();
    final Map<String, Object> fields = objectMapper.readValue(
        jobJson, new TypeReference<Map<String, Object>>() {});
    fields.put("UNKNOWN_FIELD", "FOOBAR");
    final String modifiedJobJson = objectMapper.writeValueAsString(fields);

    final Job parsedJob = parse(modifiedJobJson, Job.class);

    assertEquals(job, parsedJob);
  }

  @Test
  public void verifyCanParseJobWithMissingEnv() throws Exception {
    final Job job = Job.newBuilder()
        .setCommand(asList("foo", "bar"))
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17")
        .build();

    removeFieldAndParse(job, "env");
  }

  @Test
  public void verifyCanParseJobWithMissingMetadata() throws Exception {
    final Job job = Job.newBuilder()
        .setCommand(asList("foo", "bar"))
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17")
        .build();

    removeFieldAndParse(job, "metadata");
  }

  private static void removeFieldAndParse(final Job job, final String... fieldNames)
      throws Exception {
    final String jobJson = job.toJsonString();

    final ObjectMapper objectMapper = new ObjectMapper();
    final Map<String, Object> fields = objectMapper.readValue(
        jobJson, new TypeReference<Map<String, Object>>() {});

    for (final String field : fieldNames) {
      fields.remove(field);
    }
    final String modifiedJobJson = objectMapper.writeValueAsString(fields);

    final Job parsedJob = parse(modifiedJobJson, Job.class);

    assertEquals(job, parsedJob);
  }

  @Test
  public void verifyJobIsImmutable() {
    final List<String> expectedCommand = ImmutableList.of("foo");
    final Map<String, String> expectedEnv = ImmutableMap.of("e1", "1");
    final Map<String, String> expectedMetadata = ImmutableMap.of("foo", "bar");
    final Map<String, PortMapping> expectedPorts = ImmutableMap.of("p1", PortMapping.of(1, 2));
    final Map<ServiceEndpoint, ServicePorts> expectedRegistration =
        ImmutableMap.of(ServiceEndpoint.of("foo", "tcp"), ServicePorts.of("p1"));
    final Integer expectedGracePeriod = 240;

    final List<String> mutableCommand = Lists.newArrayList(expectedCommand);
    final Map<String, String> mutableEnv = Maps.newHashMap(expectedEnv);
    final Map<String, String> mutableMetadata = Maps.newHashMap(expectedMetadata);
    final Map<String, PortMapping> mutablePorts = Maps.newHashMap(expectedPorts);
    final Map<ServiceEndpoint, ServicePorts> mutableRegistration =
        Maps.newHashMap(expectedRegistration);

    final Job.Builder builder = Job.newBuilder()
        .setCommand(mutableCommand)
        .setEnv(mutableEnv)
        .setMetadata(mutableMetadata)
        .setPorts(mutablePorts)
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17")
        .setRegistration(mutableRegistration)
        .setGracePeriod(expectedGracePeriod);

    final Job job = builder.build();

    mutableCommand.add("bar");
    mutableEnv.put("e2", "2");
    mutableMetadata.put("some", "thing");
    mutablePorts.put("p2", PortMapping.of(3, 4));
    mutableRegistration.put(ServiceEndpoint.of("bar", "udp"), ServicePorts.of("p2"));

    builder.addEnv("added_env", "FOO");
    builder.addMetadata("added", "data");
    builder.addPort("added_port", PortMapping.of(4711));
    builder.addRegistration(ServiceEndpoint.of("added_reg", "added_proto"),
        ServicePorts.of("added_port"));
    builder.setGracePeriod(480);

    assertEquals(expectedCommand, job.getCommand());
    assertEquals(expectedEnv, job.getEnv());
    assertEquals(expectedMetadata, job.getMetadata());
    assertEquals(expectedPorts, job.getPorts());
    assertEquals(expectedRegistration, job.getRegistration());
    assertEquals(expectedGracePeriod, job.getGracePeriod());
  }

  @Test
  public void testChangingPortTagsChangesJobHash() {
    final Job j = Job.newBuilder().setName("foo").setVersion("1").setImage("foobar").build();
    final Job.Builder builder = j.toBuilder();
    final Map<String, PortMapping> ports = ImmutableMap.of("add_ports1", PortMapping.of(1234),
        "add_ports2", PortMapping.of(2345));
    final ImmutableMap.Builder<String, ServicePortParameters> servicePortsBuilder =
        ImmutableMap.builder();
    servicePortsBuilder.put("add_ports1", new ServicePortParameters(
        ImmutableList.of("tag1", "tag2")));
    servicePortsBuilder.put("add_ports2", new ServicePortParameters(
        ImmutableList.of("tag3", "tag4")));
    final ServicePorts servicePorts = new ServicePorts(servicePortsBuilder.build());
    final Map<ServiceEndpoint, ServicePorts> oldRegistration = ImmutableMap.of(
        ServiceEndpoint.of("add_service", "add_proto"), servicePorts);
    final Job job = builder.setPorts(ports).setRegistration(oldRegistration).build();

    final ImmutableMap.Builder<String, ServicePortParameters> newServicePortsBuilder =
        ImmutableMap.builder();
    newServicePortsBuilder.put("add_ports1", new ServicePortParameters(
        ImmutableList.of("tag1", "newtag")));
    newServicePortsBuilder.put("add_ports2", new ServicePortParameters(
        ImmutableList.of("tag3", "tag4")));
    final ServicePorts newServicePorts = new ServicePorts(newServicePortsBuilder.build());
    final Map<ServiceEndpoint, ServicePorts> newRegistration = ImmutableMap.of(
        ServiceEndpoint.of("add_service", "add_proto"), newServicePorts);
    final Job newJob = builder.setRegistration(newRegistration).build();

    assertNotEquals(job.getId().getHash(), newJob.getId().getHash());
  }

  @Test
  public void testBuildWithoutHash() {
    final Job.Builder builder = Job.newBuilder()
        .setCommand(asList("foo", "bar"))
        .setImage("foobar:4711")
        .setName("foozbarz")
        .setVersion("17");

    assertNull(builder.buildWithoutHash().getId().getHash());
    assertNotNull(builder.build().getId().getHash());
  }

  @Test
  public void testHasExternalPorts() {
    final Job.Builder builder = Job.newBuilder().setName("foo").setVersion("1").setImage("foobar");

    final Job noPorts = builder.build();
    assertThat(noPorts.hasExternalPorts(), equalTo(false));

    final Job noExternalPorts = builder
        .setPorts(ImmutableMap.of(
            "add_ports1", PortMapping.of(1234),
            "add_ports2", PortMapping.of(2345)
        ))
        .build();
    assertThat(noExternalPorts.hasExternalPorts(), equalTo(false));

    final Job externalPorts = builder
        .setPorts(ImmutableMap.of(
            "add_ports1", PortMapping.of(1234),
            "add_ports2", PortMapping.of(2345, 9090)
        ))
        .build();
    assertThat(externalPorts.hasExternalPorts(), equalTo(true));
  }
}
