# Makefile for compiling and running easy_stack allocator tests, fuzzers and benchmarks

UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)

ASAN_OPTS = allocator_may_return_null=1:detect_stack_use_after_return=1
SAN_FLAGS = -fsanitize=address,undefined

LSAN_RUN_FIX = LSAN_OPTIONS="detect_leaks=0"

ifeq ($(UNAME_S), Linux)
    ifeq ($(UNAME_M), x86_64)
        SAN_FLAGS += -fsanitize=leak
        ASAN_OPTS := $(ASAN_OPTS):detect_leaks=1
		LSAN_RUN_FIX = LSAN_OPTIONS="detect_leaks=1"
    else
        LSAN_RUN_FIX = ASAN_OPTIONS="$(ASAN_OPTS):detect_leaks=0" LSAN_OPTIONS="detect_leaks=0"
    endif
endif

ifneq (,$(filter MINGW% MSYS%,$(UNAME_S)))
    SAN_FLAGS = 
endif

CC ?= clang
STD_C ?= c99
BASE_CFLAGS = -Werror -Wall -Wextra \
	     -Wshadow \
		 -Wconversion -Wsign-conversion \
		 -Wundef \
		 -Wstrict-aliasing=1 \
		 -Wpointer-arith \
		 -Wdouble-promotion \
		 -Wcast-align \
		 -Wcast-qual \
		 -Wmissing-declarations \
		 -Wmissing-prototypes \
		 -Wstrict-prototypes \
		 -Wpadded \
		 -Wint-to-pointer-cast \
		 -Wpointer-to-int-cast \
		 -W -std=$(STD_C) \
		 -g3 \
		 -fno-omit-frame-pointer \
		 -fno-sanitize-recover=all \
		 -I.
CFLAGS = $(BASE_CFLAGS) $(EXTRA_CFLAGS)
DEBUG_FLAGS = -DDEBUG # Debug flag
COV_FLAGS = -O0 -fprofile-arcs -ftest-coverage --coverage # Coverage flags
LDFLAGS_COV = --coverage # Linker flag for coverage

export UBSAN_OPTIONS=halt_on_error=0:exitcode=1:print_stacktrace=1
export ASAN_OPTIONS=$(ASAN_OPTS)

TEST_DIR = tests
# Collect all tests except the specialized AVR bare-metal simulation
TEST_SRCS = $(filter-out $(TEST_DIR)/avr_test.c, $(wildcard $(TEST_DIR)/*.c))
# Generate names for coverage object files
TEST_COV_OBJS = $(TEST_SRCS:$(TEST_DIR)/%.c=$(TEST_DIR)/%.cov.o)
# Generate names for coverage executables
TEST_COV_BINS = $(TEST_SRCS:$(TEST_DIR)/%.c=$(TEST_DIR)/%_coverage)

# --- Fuzzing Configuration ---
FUZZ_DIR = fuzzers
FUZZ_SRCS = $(wildcard $(FUZZ_DIR)/*.c)
FUZZ_BINS = $(FUZZ_SRCS:%.c=%)
FUZZ_DEBUG_BINS = $(FUZZ_SRCS:%.c=%_debug)

FUZZ_FLAGS = -fsanitize=fuzzer,address,undefined -O3 -g3 -fno-omit-frame-pointer
FUZZ_DEBUG_FLAGS = -fsanitize=fuzzer,address,undefined -O0 -g3 -fno-omit-frame-pointer -DESTACK_FUZZ_DEBUG -DDEBUG

# --- Benchmark Configuration ---
DEPTH ?= 15
BENCH_DIR = bench
BENCH_SRCS = $(BENCH_DIR)/benchmark.cpp $(BENCH_DIR)/Allocator.cpp $(BENCH_DIR)/StackAllocator.cpp
BENCH_BIN = $(BENCH_DIR)/benchmark_$(DEPTH)
CXX ?= g++
CXXFLAGS = -O3 -std=c++17 -flto -DNDEBUG -Wno-stringop-overflow -DBENCH_DEPTH=$(DEPTH) -I. -I$(BENCH_DIR)

# Define the primary source file to check coverage for.
COVERAGE_SRC = easy_stack.h

.PHONY: all clean run tests tests_full list coverage build_coverage bench

# Default goal: show available commands
.DEFAULT_GOAL := list
all: list

# Compilation of each test without debug information
$(TEST_DIR)/%_silent: $(TEST_DIR)/%.c easy_stack.h $(TEST_DIR)/test_utils.h
	$(CC) $(CFLAGS) $(SAN_FLAGS) $< -o $@

# Compilation of each test with debug information
$(TEST_DIR)/%_debug: $(TEST_DIR)/%.c easy_stack.h $(TEST_DIR)/test_utils.h
	$(CC) $(CFLAGS) $(DEBUG_FLAGS) $(SAN_FLAGS) $< -o $@

# --- Coverage Build Steps ---
$(TEST_DIR)/%.cov.o: $(TEST_DIR)/%.c easy_stack.h $(TEST_DIR)/test_utils.h
	$(CC) $(CFLAGS) $(COV_FLAGS) -c $< -o $@

$(TEST_DIR)/%_coverage: $(TEST_DIR)/%.cov.o
	$(CC) $(CFLAGS) $(COV_FLAGS) $^ $(LDFLAGS_COV) -o $@
# --- End Coverage Build Steps ---

# Pattern rule for running individual tests (always with debug)
test_%: $(TEST_DIR)/%_test_debug
	@printf "\n--- Running $< (debug mode) ---\n"
	@./$<
	@if [ $$? -ne 0 ]; then \
		printf "\nTest $< FAILED!\n"; \
		exit 1; \
	else \
		printf "\nTest $< PASSED!\n"; \
	fi

# Compilation of all tests without debug
build_silent: $(TEST_SRCS:%.c=%_silent)

# Compilation of all tests with debug information
build_debug: $(TEST_SRCS:%.c=%_debug)

# Compilation of all tests with coverage information (depends on executables)
build_coverage: $(TEST_COV_BINS)

# Memory leak check using valgrind
valgrind: clean
	@printf "Running valgrind memory check on all tests...\n"
	@$(MAKE) build_silent SAN_FLAGS="" CFLAGS="$(CFLAGS) -D__valgrind__"
	@for test in $(TEST_SRCS:%.c=%_silent) ; do \
		printf "\n--- Checking $$test ---\n" ; \
		valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all --track-origins=yes ./$$test ; \
	done
	@printf "\nAll memory checks completed.\n"

# Testing: run all tests without debug info
tests: build_silent
	@printf "Running all tests (normal mode)...\n"
	@exit_code=0; \
	for test in $(TEST_SRCS:%.c=%_silent) ; do \
		printf "\n--- Running $$test ---\n" ; \
		$(LSAN_RUN_FIX) ./$$test ; \
		if [ $$? -ne 0 ]; then \
			printf "\nTest $$test FAILED with exit code $$?\n"; \
			exit_code=1; \
		fi; \
	done; \
	if [ "$$exit_code" = "1" ]; then \
		printf "\nSome tests FAILED!\n"; \
		exit 1; \
	else \
		printf "\nAll tests PASSED!\n"; \
	fi

# Testing: run all tests with debug info
tests_full: build_debug
	@printf "Running all tests (debug mode)...\n"
	@exit_code=0; \
	for test in $(TEST_SRCS:%.c=%_debug) ; do \
		printf "\n--- Running $$test ---\n" ; \
		$(LSAN_RUN_FIX) ./$$test ; \
		if [ $$? -ne 0 ]; then \
			printf "\nTest $$test FAILED with exit code $$?\n"; \
			exit_code=1; \
		fi; \
	done; \
	if [ "$$exit_code" = "1" ]; then \
		printf "\nSome tests FAILED!\n"; \
		exit 1; \
	else \
		printf "\nAll tests PASSED!\n"; \
	fi

# Coverage: build with coverage flags and run tests
coverage: clean build_coverage
	@printf "Running all tests to generate coverage data...\n"
	@exit_code=0; \
	for test in $(TEST_COV_BINS) ; do \
		printf "\n--- Running $$test (for coverage) ---\n" ; \
		./$$test ; \
		if [ $$? -ne 0 ]; then \
			printf "\nTest $$test FAILED with exit code $$?\n"; \
			exit_code=1; \
		fi; \
	done; \
	if [ "$$exit_code" = "1" ]; then \
		printf "\nSome tests FAILED! Coverage data generation might be incomplete.\n"; \
		exit 1; \
	else \
		printf "\nCoverage data generated successfully from all tests.\n"; \
	fi
	
	@printf "\nGenerating final coverage.info for Codecov...\n"
	lcov --capture --directory . --output-file coverage.info

	@printf "Filtering system files from the report...\n"
	lcov --remove coverage.info '/usr/*' '*/test_utils.h' --output-file coverage.info --ignore-errors unused

	rm -f $(TEST_COV_BINS)
	rm -f $(TEST_DIR)/*.gcda $(TEST_DIR)/*.gcno
	@rm -f coverage_base.info coverage_run.info coverage_total.info

	@printf "Successfully generated final coverage.info for Codecov.\n"


# ==========================================
# Automated Matrix Testing System
# ==========================================

MATRIX_STDS ?= c99 c11 c17 c2x
MATRIX_OPTS ?= O0 O1 O2 O3 Os Oz
MATRIX_POLS ?= 0 1
MATRIX_DIR  ?= build_matrix

# Extract bare test names
MATRIX_TEST_NAMES := $(patsubst $(TEST_DIR)/%.c,%,$(TEST_SRCS))
MATRIX_RUN_TARGETS :=

# Template to generate a specific target for each combination
define MATRIX_RULE
# Override global variables for this specific binary
$(MATRIX_DIR)/$(1)_$(2)_p$(3)/$(4): STD_C := $(1)
$(MATRIX_DIR)/$(1)_$(2)_p$(3)/$(4): EXTRA_CFLAGS := -$(2) -DESTACK_SAFETY_POLICY=$(3) -DESTACK_ASSERT_STAYS

# Compilation step
$(MATRIX_DIR)/$(1)_$(2)_p$(3)/$(4): $(TEST_DIR)/$(4).c easy_stack.h $(TEST_DIR)/test_utils.h
	@mkdir -p $$(@D)
	$$(CC) $$(CFLAGS) $$(SAN_FLAGS) $$< -o $$@ || (touch $$@.FAILED && exit 1)

# Execution step
.PHONY: run_matrix_$(1)_$(2)_p$(3)_$(4)
run_matrix_$(1)_$(2)_p$(3)_$(4): $(MATRIX_DIR)/$(1)_$(2)_p$(3)/$(4)
	@printf "\n=== Running: Std: $(1) | Opt: -$(2) | Pol: $(3) | Test: $(4) ===\n"
	@if $$(LSAN_RUN_FIX) ./$$< ; then \
		printf "[OK] Test passed.\n"; \
	else \
		printf "[FAIL] Test failed!\n"; \
		touch $$<.FAILED; \
		exit 1; \
	fi

MATRIX_RUN_TARGETS += run_matrix_$(1)_$(2)_p$(3)_$(4)
endef

# Evaluate the template for every combination
$(foreach s,$(MATRIX_STDS), \
  $(foreach o,$(MATRIX_OPTS), \
    $(foreach p,$(MATRIX_POLS), \
      $(foreach t,$(MATRIX_TEST_NAMES), \
        $(eval $(call MATRIX_RULE,$(s),$(o),$(p),$(t)))))))

.PHONY: test_matrix _matrix_run_all clean_matrix

# Internal target to depend on all runs
_matrix_run_all: $(MATRIX_RUN_TARGETS)

# Main matrix entrypoint
test_matrix:
	@mkdir -p $(MATRIX_DIR)
	@find $(MATRIX_DIR) -name "*.FAILED" -type f -delete 2>/dev/null || true
	@+$(MAKE) --no-print-directory _matrix_run_all -k || true
	@FAILURES=$$(find $(MATRIX_DIR) -name "*.FAILED" 2>/dev/null); \
	if [ -n "$$FAILURES" ]; then \
		printf "\n========================================\n"; \
		printf "         FAILED COMBINATIONS            \n"; \
		printf "========================================\n"; \
		for f in $$FAILURES; do \
			combo=$$(basename $$(dirname $$f)); \
			testname=$$(basename $$f .FAILED); \
			printf " [X] $$combo -> $$testname\n"; \
		done; \
		printf "========================================\n"; \
		exit 1; \
	else \
		printf "\n=== All $(words $(MATRIX_RUN_TARGETS)) Matrix Tests Passed ===\n"; \
	fi

clean_matrix:
	rm -rf $(MATRIX_DIR)


# --- Benchmark Target ---
$(BENCH_BIN): $(BENCH_SRCS) easy_stack.h
	@printf "Compiling benchmark suite (depth $(DEPTH)): $@\n"
	@$(CXX) $(CXXFLAGS) $(BENCH_SRCS) -o $@

bench: $(BENCH_BIN)
	@printf "\n--- Running Stack Allocator Benchmarks (Depth: $(DEPTH)) ---\n"
	@./$(BENCH_BIN)


# Cleaning binary files and coverage files
clean:
	rm -f $(TEST_SRCS:%.c=%_silent) $(TEST_SRCS:%.c=%_debug) $(TEST_COV_BINS)
	rm -f $(TEST_DIR)/*.o $(TEST_DIR)/*.cov.o # Clean object files
	rm -f *.gcov # Clean root gcov files if any generated manually
	rm -f $(TEST_DIR)/*.gcda $(TEST_DIR)/*.gcno # Clean coverage data files
	rm -f coverage.info
	rm -f $(FUZZ_BINS) $(FUZZ_DEBUG_BINS)
	rm -rf $(MATRIX_DIR)
	rm -f $(BENCH_DIR)/benchmark_* # Clean all benchmark binaries


# --- Fuzzing Targets ---
$(FUZZ_DIR)/%: CC = clang
$(FUZZ_DIR)/%_debug: CC = clang

$(FUZZ_DIR)/%: $(FUZZ_DIR)/%.c easy_stack.h $(FUZZ_DIR)/fuzz_utils.h
	@printf "Compiling fuzzer: $@\n"
	@$(CC) $(BASE_CFLAGS) $(FUZZ_FLAGS) $< -o $@

$(FUZZ_DIR)/%_debug: $(FUZZ_DIR)/%.c easy_stack.h $(FUZZ_DIR)/fuzz_utils.h
	@printf "Compiling fuzzer replay mode: $@\n"
	@$(CC) $(BASE_CFLAGS) $(FUZZ_DEBUG_FLAGS) $< -o $@

FUZZ_TIME ?= 300
fuzz_%: $(FUZZ_DIR)/%_fuzzer
	@printf "\n--- Running Fuzzer: $< (Timeout: $(FUZZ_TIME)s) ---\n"
	@./$< -max_total_time=$(FUZZ_TIME)

replay_%: $(FUZZ_DIR)/%_fuzzer_debug
	@if [ -z "$(CRASH)" ]; then \
		printf "\nERROR: You must specify the crash file!\n"; \
		printf "Usage: make replay_$* CRASH=crash-filename\n\n"; \
		exit 1; \
	fi
	@printf "\n--- Replaying crash file: $(CRASH) on $< ---\n"
	@./$< $(CRASH)

# Show available tests
list:
	@printf "Available commands:\n"
	@printf "  make tests                    - run all tests without debug output \n"
	@printf "  make tests_full               - run all tests with debug output\n"
	@printf "  make test_matrix -j$(nproc)   - run matrix of tests\n"
	@printf "  make bench [DEPTH=15|30|100]  - compile and run benchmarks with customizable stack depth (default: 15)\n"
	@printf "  make coverage                 - build & run tests to generate coverage data for CodeCov\n"
	@printf "  make fuzz_[name]              - run the 'core' fuzzer for 5 minutes (auto-detects fuzz_*.c)\n"
	@printf "  make replay_[name] CRASH=...  - replay a specific crash file with ASCII visualization\n"
	@printf "\nAvailable individual tests (always with debug output):\n"
	@for test in $(TEST_SRCS) ; do \
		basename=$$(basename $${test%.c} _test); \
		printf "  make test_$$basename\n" ; \
	done
	@printf "\nAvailable individual fuzzers:\n"
	@for test in $(FUZZ_SRCS) ; do \
		basename=$$(basename $${test%.c} _fuzzer); \
		printf "  make fuzz_$$basename\n" ; \
	done