package de.effigenix.domain.production;

import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;

/**
 * Fuzz test for BatchTraceabilityService.
 *
 * Generates random batch graphs (including cycles, diamonds, deep chains)
 * and verifies that traceForward always terminates without exceptions.
 * Uses an in-memory BatchRepository stub to build arbitrary graph topologies.
 *
 * Run: make fuzz  |  make fuzz/single TEST=BatchTraceabilityServiceFuzzTest
 */
class BatchTraceabilityServiceFuzzTest {

    @FuzzTest(maxDuration = "5m")
    void fuzzTraceBackward(FuzzedDataProvider data) {
        int nodeCount = data.consumeInt(1, 30);
        List<String> nodeIds = new ArrayList<>();
        for (int i = 0; i < nodeCount; i++) {
            nodeIds.add("batch-" + i);
        }

        Map<String, List<String>> adjacency = new HashMap<>();
        for (String nodeId : nodeIds) {
            int childCount = data.consumeInt(0, Math.min(5, nodeCount));
            List<String> children = new ArrayList<>();
            for (int c = 0; c < childCount; c++) {
                int childIdx = data.consumeInt(0, nodeCount - 1);
                children.add(nodeIds.get(childIdx));
            }
            adjacency.put(nodeId, children);
        }

        var repo = new InMemoryGraphRepository(nodeIds, adjacency);
        var service = new BatchTraceabilityService(repo);

        String startId = nodeIds.get(data.consumeInt(0, nodeCount - 1));
        int maxDepth = data.consumeInt(0, 15);

        var result = service.traceBackward(BatchId.of(startId), maxDepth);

        switch (result) {
            case Result.Success(var traced) -> {
                for (TracedBatch tb : traced) {
                    assert tb.depth() >= 1 && tb.depth() <= maxDepth;
                }
                Set<String> seen = new HashSet<>();
                for (TracedBatch tb : traced) {
                    assert seen.add(tb.batch().id().value()) : "Duplicate batch in result";
                }
                for (TracedBatch tb : traced) {
                    assert !tb.batch().id().value().equals(startId) : "Start batch in result";
                }
            }
            case Result.Failure(var err) -> {
                assert err != null;
            }
        }
    }

    @FuzzTest(maxDuration = "5m")
    void fuzzTraceForward(FuzzedDataProvider data) {
        int nodeCount = data.consumeInt(1, 30);
        List<String> nodeIds = new ArrayList<>();
        for (int i = 0; i < nodeCount; i++) {
            nodeIds.add("batch-" + i);
        }

        // Build random adjacency: for each node, pick random children
        Map<String, List<String>> adjacency = new HashMap<>();
        for (String nodeId : nodeIds) {
            int childCount = data.consumeInt(0, Math.min(5, nodeCount));
            List<String> children = new ArrayList<>();
            for (int c = 0; c < childCount; c++) {
                int childIdx = data.consumeInt(0, nodeCount - 1);
                children.add(nodeIds.get(childIdx));
            }
            adjacency.put(nodeId, children);
        }

        var repo = new InMemoryGraphRepository(nodeIds, adjacency);
        var service = new BatchTraceabilityService(repo);

        // Pick random start node
        String startId = nodeIds.get(data.consumeInt(0, nodeCount - 1));
        int maxDepth = data.consumeInt(0, 15);

        var result = service.traceForward(BatchId.of(startId), maxDepth);

        // Must always terminate and return a valid Result — never throw
        switch (result) {
            case Result.Success(var traced) -> {
                // All depths must be within bounds
                for (TracedBatch tb : traced) {
                    assert tb.depth() >= 1 && tb.depth() <= maxDepth;
                }
                // No duplicates
                Set<String> seen = new HashSet<>();
                for (TracedBatch tb : traced) {
                    assert seen.add(tb.batch().id().value()) : "Duplicate batch in result";
                }
                // Start batch must not be in result
                for (TracedBatch tb : traced) {
                    assert !tb.batch().id().value().equals(startId) : "Start batch in result";
                }
            }
            case Result.Failure(var err) -> {
                // Failures are acceptable (e.g., RepositoryFailure), but must not be null
                assert err != null;
            }
        }
    }

    private static Batch makeBatch(String id) {
        return Batch.reconstitute(
                BatchId.of(id),
                BatchNumber.generate(LocalDate.of(2026, 1, 1), 1),
                RecipeId.of("recipe-fuzz"),
                BatchStatus.COMPLETED,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                null, null, null,
                LocalDate.of(2026, 1, 1),
                LocalDate.of(2026, 6, 1),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                null, null, null,
                0L, List.of()
        );
    }

    /**
     * In-memory BatchRepository that serves a pre-built graph.
     * Implements traceForward with BFS (mirrors the contract of the recursive CTE).
     */
    private static class InMemoryGraphRepository implements BatchRepository {

        private final Set<String> nodeIds;
        private final Map<String, List<String>> adjacency;

        InMemoryGraphRepository(List<String> nodeIds, Map<String, List<String>> adjacency) {
            this.nodeIds = new HashSet<>(nodeIds);
            this.adjacency = adjacency;
        }

        @Override
        public Result<RepositoryError, Optional<Batch>> findById(BatchId id) {
            if (nodeIds.contains(id.value())) {
                return Result.success(Optional.of(makeBatch(id.value())));
            }
            return Result.success(Optional.empty());
        }

        @Override
        public Result<RepositoryError, List<Batch>> findByInputBatchId(BatchId inputBatchId) {
            var children = adjacency.getOrDefault(inputBatchId.value(), List.of());
            return Result.success(children.stream().map(BatchTraceabilityServiceFuzzTest::makeBatch).toList());
        }

        @Override
        public Result<RepositoryError, List<TracedBatch>> traceForward(BatchId startBatchId, int maxDepth) {
            List<TracedBatch> result = new ArrayList<>();
            Set<String> visited = new HashSet<>();
            visited.add(startBatchId.value());

            record BfsEntry(String batchId, int depth) {}
            var queue = new ArrayDeque<BfsEntry>();
            queue.add(new BfsEntry(startBatchId.value(), 0));

            while (!queue.isEmpty()) {
                var entry = queue.poll();
                if (entry.depth() >= maxDepth) continue;

                var children = adjacency.getOrDefault(entry.batchId(), List.of());
                int childDepth = entry.depth() + 1;
                for (String childId : children) {
                    if (visited.add(childId)) {
                        result.add(new TracedBatch(makeBatch(childId), childDepth));
                        queue.add(new BfsEntry(childId, childDepth));
                    }
                }
            }

            return Result.success(result);
        }

        @Override
        public Result<RepositoryError, List<TracedBatch>> traceBackward(BatchId startBatchId, int maxDepth) {
            // Invert adjacency: for traceBackward, find parents (nodes whose children include startBatchId)
            Map<String, List<String>> reverseAdj = new HashMap<>();
            for (var entry : adjacency.entrySet()) {
                for (String child : entry.getValue()) {
                    reverseAdj.computeIfAbsent(child, k -> new ArrayList<>()).add(entry.getKey());
                }
            }

            List<TracedBatch> result = new ArrayList<>();
            Set<String> visited = new HashSet<>();
            visited.add(startBatchId.value());

            record BfsEntry(String batchId, int depth) {}
            var queue = new ArrayDeque<BfsEntry>();
            queue.add(new BfsEntry(startBatchId.value(), 0));

            while (!queue.isEmpty()) {
                var entry = queue.poll();
                if (entry.depth() >= maxDepth) continue;

                var parents = reverseAdj.getOrDefault(entry.batchId(), List.of());
                int parentDepth = entry.depth() + 1;
                for (String parentId : parents) {
                    if (visited.add(parentId)) {
                        result.add(new TracedBatch(makeBatch(parentId), parentDepth));
                        queue.add(new BfsEntry(parentId, parentDepth));
                    }
                }
            }

            return Result.success(result);
        }

        // --- Remaining methods are not used by BatchTraceabilityService ---

        @Override public Result<RepositoryError, List<Batch>> findAll() { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, Optional<Batch>> findByBatchNumber(BatchNumber n) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findByStatus(BatchStatus s) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findByProductionDate(LocalDate d) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findByRecipeIds(List<RecipeId> ids) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findAllSummary() { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findByStatusSummary(BatchStatus s) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findByProductionDateSummary(LocalDate d) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, List<Batch>> findByRecipeIdsSummary(List<RecipeId> ids) { throw new UnsupportedOperationException(); }
        @Override public Result<RepositoryError, Void> save(Batch batch) { throw new UnsupportedOperationException(); }
    }
}
