# Makefile for Firehose app

MISE_BIN ?= mise
MISE_EXEC = $(MISE_BIN) exec --

.PHONY: check precommit deps assets compile test test.coverage coverage crap format credo verify

# Run all static analysis checks (no database required)
check: credo format

# Precommit target for CI/pre-commit hooks
precommit: check compile

# Install JS dependencies (esbuild bundle, playwright). `mix phx.server` and
# `mix assets.deploy` need them: without `assets/node_modules` the bundle fails
# to resolve highlight.js and LiveView never loads.
#
# Guarded on npm's own install marker, so a clone pays for it once and later
# runs never touch the network: `make test` keeps working offline.
assets:
	@test -f assets/node_modules/.package-lock.json || \
		(echo "==> assets: npm install (first run)"; cd assets && npm install)

# Sync dependencies (Hex packages + JS packages)
deps: assets
	$(MISE_EXEC) mix deps.get

# Compile the project
compile:
	$(MISE_EXEC) mix compile --warnings-as-errors

# Run tests (requires PostgreSQL running on localhost:5432)
# Note: If you don't have PostgreSQL, you can skip tests with `make check`
test: deps compile
	$(MISE_EXEC) mix test

# Run the suite once with coverage exported, so the coverage floor check (and,
# later, CRAP) reads the export instead of running the suite a second time.
# Stale exports are removed first: the floor must never report an older run.
test.coverage: deps compile
	rm -f cover/*.coverdata
	$(MISE_EXEC) mix test --cover --export-coverage default

# Enforce the coverage floor (mix.exs -> test_coverage) from that export. No
# test run. `mix test.coverage` exits 0 when it finds no export at all, so the
# guard below is load-bearing: a floor check that did not run must not pass.
coverage:
	@test -f cover/default.coverdata || { echo "coverage: no exported coverage - run 'make test.coverage' first" >&2; exit 1; }
	$(MISE_EXEC) mix test.coverage

# Enforce CRAP scores (complexity x coverage) from the same export. No test run.
# Runs in MIX_ENV=test because that is the environment the export came from:
# running in dev would compile a second build and score against a different set
# of modules. The guard is load-bearing for the same reason as `coverage`:
# ex_crap falls back to 0% coverage when the export is missing, which would
# report scores that are pessimistic instead of absent - a false failure is
# less bad than a false pass, but neither is acceptable in the Lens pass.
#
# Threshold is ex_crap's default (30); whether to ratchet it down is an open
# decision tracked in the "ratchets, deferred decisions, handoff practice" yak.
crap:
	@test -f cover/default.coverdata || { echo "crap: no exported coverage - run 'make test.coverage' first" >&2; exit 1; }
	MIX_ENV=test $(MISE_EXEC) mix crap

# Format code
format:
	$(MISE_EXEC) mix format

# =============================================================================
# Playwright e2e browser tests
# =============================================================================
#
# Requires the Phoenix dev server running on the default port (8056).
# Set PLAYWRIGHT_BASE_URL to point at a different server/port.
#
# Available browsers (local):
#   PLAYWRIGHT_BROWSER=firefox   (default — works on macOS 15)
#   PLAYWRIGHT_BROWSER=chromium  (crashes on macOS 15, use Docker Chrome)
#   PLAYWRIGHT_BROWSER=webkit
#
# Docker Chrome (alternative):
#   1. docker compose -f docker-compose.chrome.yml up -d
#   2. PLAYWRIGHT_BROWSERS_PATH=assets/node_modules/playwright-core/.local-browsers \
#      npx playwright test --config=playwright.docker.config.ts
#
# =============================================================================

# Run Playwright e2e tests (default: local Firefox)
# Usage: make playwright
#        PLAYWRIGHT_BROWSER=chromium make playwright
playwright:
	cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts

# Run a specific Playwright test file
# Usage: make playwright-test test/e2e/smoke.spec.ts
playwright-test:
	cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts $(filter-out $@,$(MAKECMDGOALS))

# Run Playwright tests with UI mode (headed browser)
playwright-ui:
	cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts --ui

# Show last Playwright HTML report
playwright-report:
	cd assets && npx playwright show-report

# Start Docker Chrome container for remote Playwright
playwright-docker-start:
	docker compose -f docker-compose.chrome.yml up -d

# Stop Docker Chrome container
playwright-docker-stop:
	docker compose -f docker-compose.chrome.yml down

# Run Playwright against Docker Chrome (container must be running)
playwright-docker:
	cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.docker.config.ts

# Run full end-to-end workflow: start Docker Chrome, run tests, stop container
playwright-full: playwright-docker-start
	@echo "Waiting for Chrome to be ready..."
	@sleep 3
	cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.docker.config.ts; \
		EXIT_CODE=$$?; \
		cd ..; \
		docker compose -f docker-compose.chrome.yml down; \
		exit $$EXIT_CODE

# =============================================================================
# Static analysis
# =============================================================================

# Run Credo static analysis
credo:
	$(MISE_EXEC) mix credo --strict

# =============================================================================
# Lens pass (heavy checks; root `make verify` runs this before every push)
# =============================================================================
#
# Sensor slots, cheapest first. Credo is cheap enough that it lives in
# `check`, not here. Add a sensor as its yak lands, both as a target and to
# VERIFY_SENSORS — that variable is the single source of truth for what this
# target reports as wired.
#   - sobelow    (yak: sobelow in the Lens pass)
#   - coverage   (yak: coverage floors + framework-glue exclusions policy)
#                  WIRED below (test.coverage + coverage).
#   - crap       (yak: CRAP scores with ex_crap (both apps))
#                  WIRED below (crap); reuses the coverage export.
#   - mutation   (yak: mutation testing with muex — a sensor, not a gate)
#
# Runtime note: the coverage sensor exports from a single suite run
# (`test.coverage` -> `mix test --cover --export-coverage default`) that CRAP
# reuses, instead of running the suite twice.
#
# The suite runs FIRST, because it is what produces the coverage export the
# sensors read; sensors still run cheapest-first among themselves.
VERIFY_SENSORS := coverage crap

verify:
	@echo "==> verify(app): suite (coverage exported), then sensors [$(VERIFY_SENSORS)]"
	@$(MAKE) test.coverage
	@for sensor in $(VERIFY_SENSORS); do echo "==> verify(app): sensor $$sensor"; $(MAKE) $$sensor || exit 1; done
