UseCaseTest.java

package com.guinetik.hexafun.testing;

import com.guinetik.hexafun.HexaApp;
import com.guinetik.hexafun.fun.Result;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * A fluent API for testing HexaFun use cases.
 * @param <I> The input type of the use case
 * @param <O> The output type of the use case
 */
public class UseCaseTest<I, O> {
    private final HexaApp app;
    private final String useCaseName;
    private I input;
    private O output;
    private Exception exception;
    private boolean executed = false;

    UseCaseTest(HexaApp app, String useCaseName) {
        this.app = app;
        this.useCaseName = useCaseName;
    }

    /**
     * Specify the input for the use case.
     * @param input The input to provide to the use case
     * @return This test instance
     */
    public UseCaseTest<I, O> with(I input) {
        this.input = input;
        return this;
    }

    /**
     * Execute the use case and verify the result is successful.
     * @param verifier A consumer that receives the result for verification
     * @return This test instance
     */
    public UseCaseTest<I, O> expectOk(Consumer<O> verifier) {
        executeIfNeeded();
        if (exception != null) {
            throw new AssertionError("Expected successful result but got exception: " + exception.getMessage(), exception);
        }
        
        if (output instanceof Result) {
            Result<?> result = (Result<?>) output;
            if (result.isFailure()) {
                throw new AssertionError("Expected successful result but got failure: " + result.error());
            }
            @SuppressWarnings("unchecked")
            O unwrapped = (O) result.get();
            verifier.accept(unwrapped);
        } else {
            verifier.accept(output);
        }
        
        return this;
    }

    /**
     * Execute the use case and verify the result is a failure.
     * @param errorVerifier A consumer that receives the error message for verification
     * @return This test instance
     */
    public UseCaseTest<I, O> expectFailure(Consumer<String> errorVerifier) {
        executeIfNeeded();
        if (exception != null) {
            errorVerifier.accept(exception.getMessage());
            return this;
        }
        
        if (output instanceof Result) {
            Result<?> result = (Result<?>) output;
            if (result.isSuccess()) {
                throw new AssertionError("Expected failure but got successful result: " + result.get());
            }
            errorVerifier.accept(result.error());
        } else {
            throw new AssertionError("Expected Result type but got: " + output.getClass().getName());
        }
        
        return this;
    }

    /**
     * Execute the use case and verify the result matches a predicate.
     * @param predicate A predicate to test the result
     * @param description Description of what the predicate checks
     * @return This test instance
     */
    public UseCaseTest<I, O> expect(Predicate<O> predicate, String description) {
        executeIfNeeded();
        if (exception != null) {
            throw new AssertionError("Expected result but got exception: " + exception.getMessage(), exception);
        }
        
        if (!predicate.test(output)) {
            throw new AssertionError("Expected " + description + " but was not satisfied by " + output);
        }
        
        return this;
    }

    /**
     * Execute the use case and verify it throws a specific exception.
     * @param exceptionClass Expected exception class
     * @return This test instance
     */
    public UseCaseTest<I, O> expectException(Class<? extends Exception> exceptionClass) {
        executeIfNeeded();
        if (exception == null) {
            throw new AssertionError("Expected exception of type " + exceptionClass.getName() + " but no exception was thrown");
        }
        
        if (!exceptionClass.isInstance(exception)) {
            throw new AssertionError("Expected exception of type " + exceptionClass.getName() 
                + " but got " + exception.getClass().getName(), exception);
        }
        
        return this;
    }

    /**
     * Map the result using a transformer function for further verification.
     * @param mapper Function to transform the result
     * @param <T> Target type
     * @return A new test instance with the transformed result
     */
    public <T> UseCaseTest<I, T> map(Function<O, T> mapper) {
        executeIfNeeded();
        if (exception != null) {
            throw new AssertionError("Cannot map result: an exception occurred: " + exception.getMessage(), exception);
        }
        
        UseCaseTest<I, T> mappedTest = new UseCaseTest<>(app, useCaseName);
        mappedTest.input = this.input;
        mappedTest.executed = true;
        
        try {
            mappedTest.output = mapper.apply(output);
        } catch (Exception e) {
            mappedTest.exception = e;
        }
        
        return mappedTest;
    }

    /**
     * Execute the use case if it hasn't been executed yet.
     */
    private void executeIfNeeded() {
        if (executed) {
            return;
        }

        try {
            output = app.invokeByName(useCaseName, input);
        } catch (Exception e) {
            exception = e;
        }

        executed = true;
    }
}