package eu.reply.lea.services.yard.entity;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.time.Instant;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import eu.reply.lea.services.yard.event.YardContentCompletedMovementEvent;
import eu.reply.lea.services.yard.event.YardContentOutEvent;
import eu.reply.lea.services.yard.event.YardContentStartedMovementEvent;
import eu.reply.sce.backend.core.event.ConfigurableEventProducer;

public class YardContentTest {


    @Mock
    private ConfigurableEventProducer<YardContentStartedMovementEvent> yardContentStartedMovementProducer;
    @Mock
    private ConfigurableEventProducer<YardContentCompletedMovementEvent> yardContentCompletedMovementProducer;
    @Mock
    private ConfigurableEventProducer<YardContentOutEvent> yardContentOutProducer;
    private AutoCloseable closeable;
    @BeforeEach
    public void setup() {
        closeable = MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void closeService() throws Exception {
        closeable.close();
    }

    @Test
    void testMoveToLocation() {
        YardLocation yardLocation = new YardLocation(null, LocStatus.OPEN, false, 0);

        YardContent content = new YardContent();
        content.move(yardLocation, Instant.now(), yardContentCompletedMovementProducer);
        assertFalse(content.isMoving());
        assertEquals(content.getLocation(), yardLocation);
    }


    @Test
    void testMoveToNullLocation() {
        YardContent content = new YardContent();
        assertThrows(NullPointerException.class, () -> {
            content.move(null, Instant.now(), yardContentCompletedMovementProducer);
        });
    }

    @Test
    void testMoveToLocationAndReplaceItWithANewOne() {
        YardLocation yardLocation1 = new YardLocation(null, LocStatus.OPEN, false, 0);


        YardContent content = new YardContent();
        content.move(yardLocation1, Instant.now(), yardContentCompletedMovementProducer);

        assertEquals(content.getLocation(), yardLocation1);
        assertFalse(content.isMoving());
        assertEquals(1, yardLocation1.getContentCount());

        YardLocation yardLocation2 = new YardLocation(null, LocStatus.OPEN, false, 0);
        content.move(yardLocation2, Instant.now(), yardContentCompletedMovementProducer);

        assertEquals(content.getLocation(), yardLocation2);
        assertFalse(content.isMoving());
        assertEquals(1, yardLocation2.getContentCount());
        assertEquals(0, yardLocation1.getContentCount());
    }

    @Test
    void testStartMovementToLocation() {
        YardLocation yardLocation = new YardLocation(null, LocStatus.OPEN, false, 0);

        YardContent content = new YardContent();
        content.move(yardLocation, Instant.now(), yardContentCompletedMovementProducer);
        assertFalse(content.isMoving());
        assertEquals(content.getLocation(), yardLocation);
    }

    @Test
    void testStartMovementToNullLocation() {
        YardContent content = new YardContent();
        assertThrows(NullPointerException.class, () -> {
            content.startMovement(null, Instant.now(), yardContentStartedMovementProducer);
        });
    }

    @Test
    void testStartMovementToLocationAndReplaceItWithANewOne() {
        YardLocation yardLocation1 = new YardLocation(null, LocStatus.OPEN, false, 0);


        YardContent content = new YardContent();
        content.startMovement(yardLocation1, Instant.now(), yardContentStartedMovementProducer);

        assertEquals(content.getLocation(), yardLocation1);
        assertTrue(content.isMoving());
        assertEquals(1, yardLocation1.getContentCount());

        YardLocation yardLocation2 = new YardLocation(null, LocStatus.OPEN, false, 0);
        content.move(yardLocation2, Instant.now(), yardContentCompletedMovementProducer);

        assertEquals(content.getLocation(), yardLocation2);
        assertFalse(content.isMoving());
        assertEquals(1, yardLocation2.getContentCount());
        assertEquals(0, yardLocation1.getContentCount());
    }

    @Test
    void testOut() {
        YardLocation yardLocation1 = new YardLocation(null, LocStatus.OPEN, false, 0);

        YardContent content = new YardContent();
        content.move(yardLocation1, Instant.now(), yardContentCompletedMovementProducer);

        assertEquals(content.getLocation(), yardLocation1);
        assertFalse(content.isMoving());
        assertEquals(1, yardLocation1.getContentCount());

        content.out(Instant.now(), yardContentOutProducer);

        assertNull(content.getLocation());
        assertFalse(content.isMoving());
        assertEquals(0, yardLocation1.getContentCount());
    }


}
package eu.reply.lea.services.yard.entity;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.time.Instant;
import java.util.Objects;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import eu.reply.lea.services.yard.event.YardContentCompletedMovementEvent;
import eu.reply.lea.services.yard.event.YardContentOutEvent;
import eu.reply.lea.services.yard.event.YardContentStartedMovementEvent;
import eu.reply.lea.services.yard.util.ColSizes;
import eu.reply.lea.services.yard.util.ZeroTrimmer;
import eu.reply.sce.backend.core.entity.JsonEntityReference;
import eu.reply.sce.backend.core.entity.StringCustomizableEntity;
import eu.reply.sce.backend.core.event.ConfigurableEventProducer;

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@NamedQueries({@NamedQuery(name = YardContent.Q_BY_ENTITY_REF, query = "select yc from YardContent yc where "
        + "yc.logisticUnit.module = :module "
        + "and yc.logisticUnit.entityName = :entityName "
        + "and yc.logisticUnit.entityId = :entityId")})
public class YardContent extends StringCustomizableEntity {

    public static final String Q_BY_ENTITY_REF = "YardContent.Q_BY_ENTITY_REF";

    public enum ContainerStatus {
        GOOD, DAMAGED;
    }

    public enum PriorityLevel {
        LOW, MEDIUM, HIGH;
    }


    @Column(length = ColSizes.CODE)
    private String code;

    @Column(length = ColSizes.CODE)
    private String visibleLabel;

    @AttributeOverrides({
        @AttributeOverride(column = @Column(name = "logUnit_businesskey", length = ColSizes.ER_BK), name = "businessKey"),
        @AttributeOverride(column = @Column(name = "logUnit_entityid", length = ColSizes.ER_ID), name = "entityId"),
        @AttributeOverride(column = @Column(name = "logUnit_entityname", length = ColSizes.ER_ENT), name = "entityName"),
        @AttributeOverride(column = @Column(name = "logUnit_module", length = ColSizes.ER_MOD), name = "module"),
        @AttributeOverride(column = @Column(name = "logUnit_description", length = ColSizes.ER_DESC), name = "description")})
    @Embedded
    private JsonEntityReference logisticUnit;

    @Column(length = ColSizes.CODE)
    private String siteCode;

    @Column(length = ColSizes.ENUM)
    @Enumerated(EnumType.STRING)
    private PriorityLevel priorityLevel;

    @Column(length = ColSizes.ENUM)
    @Enumerated(EnumType.STRING)
    private ContainerStatus containerStatus;

    private boolean emptyContainer;

    private boolean moving;

    // level for stacks
    @Min(value = 0)
    @Max(value = 99)
    private Integer level;

    /**
     * Number of containers in a transit
     */
    private Integer containerCount;

    /**
     * Activates look and feel with colored header on FE.
     */
    private boolean transitStyle;

    @Embedded
    private YardContentChangeTracker yardContentChangeTracker;

    @ManyToOne
    private YardLocation location;

    public YardContent(final JsonEntityReference logisticUnit, final Boolean isTransit, final Instant initializedOn) {
        this.code = logisticUnit.getBusinessKey();
        this.logisticUnit = logisticUnit;
        this.transitStyle = isTransit;
        this.containerCount = 0;
        this.moving = false;
        this.updateVisibleLabel();
        this.yardContentChangeTracker = new YardContentChangeTracker(initializedOn);
    }

    private void updateVisibleLabel() {
        String label = code != null ? code : (logisticUnit != null ? logisticUnit.getBusinessKey() : "");
        this.visibleLabel = ZeroTrimmer.trimLeadingZeros(label);
    }

    public void addLoadingUnit() {
        if (!this.transitStyle) {
            throw new IllegalStateException("It's only possible to add a loading unit to a transit");
        }
        this.containerCount++;
    }

    public void removeLoadingUnit() {
        if (!this.transitStyle) {
            throw new IllegalStateException("It's only possible to add a loading unit to a transit");
        }
        this.containerCount = this.containerCount == 0 ? 0 : this.containerCount - 1;
    }


    /**
     * Updates the location of the YardContent if allowed by the change tracker.
     *
     * @param newLocation the new YardLocation
     * @param occurredOn  the timestamp of the change
     */
    private void updateLocation(final YardLocation newLocation,final Instant occurredOn) {
        Objects.requireNonNull(occurredOn, "Change time must be provided.");

        if (this.yardContentChangeTracker.canChangeLocation(occurredOn)) {
            if (this.getLocation() != null) {
                this.location.removeContent(this);
            }
            this.location = newLocation;
            if (newLocation != null) {
                this.location.addContent(this);
            }
            this.yardContentChangeTracker = this.yardContentChangeTracker.locationChangedOn(occurredOn);
        } else {
            throw new IllegalStateException("Location cannot be changed because a more recent change has occurred.");
        }
    }

    /**
     * Updates the moving status of the YardContent if allowed by the change tracker.
     *
     * @param isMoving   the new moving status
     * @param occurredOn the timestamp of the change
     */
    public void updateMovingStatus(boolean isMoving, final Instant occurredOn) {
        Objects.requireNonNull(occurredOn, "Change time must be provided.");

        if (this.yardContentChangeTracker.canChangeMoving(occurredOn)) {
            this.moving = isMoving;
            this.yardContentChangeTracker = this.yardContentChangeTracker.movingChangedOn(occurredOn);
        } else {
            throw new IllegalStateException("Moving status cannot be changed because a more recent change has occurred.");
        }
    }

    public void move(final YardLocation location, final Instant occurredOn, final ConfigurableEventProducer<YardContentCompletedMovementEvent> eventProducer) {
        updateLocation(location, occurredOn);
        completeMovement(occurredOn, eventProducer);
    }

    public void completeMovement(final Instant occurredOn, final ConfigurableEventProducer<YardContentCompletedMovementEvent> eventProducer) {
        this.updateMovingStatus(false, occurredOn);
        eventProducer.fire(new YardContentCompletedMovementEvent(this));

    }

    public void startMovement(final YardLocation location, final Instant occurredOn, final ConfigurableEventProducer<YardContentStartedMovementEvent> eventProducer) {
        updateLocation(location,occurredOn);
        updateMovingStatus(true, occurredOn);
        eventProducer.fire(new YardContentStartedMovementEvent(this));
    }

    public void out(final Instant occurredOn, final ConfigurableEventProducer<YardContentOutEvent> eventProducer) {
        if (this.getLocation() != null) {
            YardLocation lastLocation = this.getLocation();
            this.updateLocation(null, occurredOn);
            eventProducer.fire(new YardContentOutEvent(this, lastLocation));
        }
    }
}
package eu.reply.lea.services.yard.entity;

import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.time.Instant;
import java.util.Objects;

@Embeddable
public class YardContentChangeTracker implements Serializable {

    private Instant locationChangedOn;
    private Instant movingChangedOn;

    public YardContentChangeTracker(Instant initializedOn) {
        Objects.requireNonNull(initializedOn, "Initialization date must be provided.");
        this.locationChangedOn = initializedOn;
        this.movingChangedOn = initializedOn;
    }

    protected YardContentChangeTracker() {
        // Default constructor for JPA
    }

    /**
     * Determines if the location can be changed at the given timestamp.
     * The location can be changed if neither the location nor the moving status
     * has been modified after the provided timestamp.
     *
     * @param asOfDate the timestamp to check against
     * @return true if the location can be changed, false otherwise
     */
    public boolean canChangeLocation(Instant asOfDate) {
        Objects.requireNonNull(asOfDate, "Comparison date must be provided.");
        return !locationChangedOn.isAfter(asOfDate) && !movingChangedOn.isAfter(asOfDate);
    }

    /**
     * Determines if the moving status can be changed at the given timestamp.
     * The moving status can be changed if neither the moving status nor the location
     * has been modified after the provided timestamp.
     *
     * @param asOfDate the timestamp to check against
     * @return true if the moving status can be changed, false otherwise
     */
    public boolean canChangeMoving(Instant asOfDate) {
        Objects.requireNonNull(asOfDate, "Comparison date must be provided.");
        return !movingChangedOn.isAfter(asOfDate) && !locationChangedOn.isAfter(asOfDate);
    }

    /**
     * Returns a new instance of YardContentChangeTracker with updated locationChangedOn timestamp.
     *
     * @param asOfDate the timestamp when the location was changed
     * @return a new YardContentChangeTracker instance with updated locationChangedOn
     */
    public YardContentChangeTracker locationChangedOn(Instant asOfDate) {
        Objects.requireNonNull(asOfDate, "Date must be provided.");
        return new YardContentChangeTracker(asOfDate, this.movingChangedOn);
    }

    /**
     * Returns a new instance of YardContentChangeTracker with updated movingChangedOn timestamp.
     *
     * @param asOfDate the timestamp when the moving status was changed
     * @return a new YardContentChangeTracker instance with updated movingChangedOn
     */
    public YardContentChangeTracker movingChangedOn(Instant asOfDate) {
        Objects.requireNonNull(asOfDate, "Date must be provided.");
        return new YardContentChangeTracker(this.locationChangedOn, asOfDate);
    }

    private YardContentChangeTracker(Instant locationChangedOn, Instant movingChangedOn) {
        this.locationChangedOn = locationChangedOn;
        this.movingChangedOn = movingChangedOn;
    }

    public Instant getLocationChangedOn() {
        return this.locationChangedOn;
    }

    public Instant getMovingChangedOn() {
        return this.movingChangedOn;
    }


    @Override
    public int hashCode() {
        return (8325 * 67)+ Objects.hash(locationChangedOn, movingChangedOn);
    }

    @Override
    public boolean equals(Object anObject) {
        boolean equalObjects = false;

        if (anObject != null && this.getClass() == anObject.getClass()) {
            YardContentChangeTracker typedObject = (YardContentChangeTracker) anObject;
            equalObjects =
                this.getLocationChangedOn().equals(typedObject.getLocationChangedOn())
                    && this.getMovingChangedOn().equals(typedObject.getLocationChangedOn());
        }

        return equalObjects;
    }


}

I have made some changes (the introductiono of the tracker)
Can you add the test for the changes?