# Copyright 2026 PingCAP, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Toolchain pins (exact; see README.md):
#   controller-runtime v0.24.1 (runtime dependency in go.mod)
#   controller-gen    v0.21.0 (generator, installed into service-local bin/)
#   setup-envtest     exact pseudo-version (see ENVTEST_VERSION)
#   helm              v3.18.5 (chart tooling; same version as dataplane-manager's Helm library)
#   kubebuilder       v4.15.0 (generation reference only; PROJECT file records cliVersion)
#   kind              v0.32.0 (local and install-smoke clusters)
#   kubectl           v1.36.1 (aligned with the persistent Kind node image)
#   MetalLB           v0.16.1 (local registry LoadBalancer)
#   BuildKit          v0.31.2 (in-cluster rootless image builder)
#   Go directive      go 1.26.5 (aligned with the repo's internal services)

.DEFAULT_GOAL := build

BINARY_DIR := $(CURDIR)/build/bin
BINARY := $(BINARY_DIR)/capacity-planner
EVALUATION_EXPERIMENT ?= test/evaluation/experiments/tidb/cost-optimized-horizontal-v1.yaml
EVALUATION_OUTPUT ?= $(BINARY_DIR)/evaluation
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
# Single version source for both the native binary and the container image.
VERSION ?= v0.0.0
IMAGE_NAME ?= capacity-planner
IMAGE_TAG ?= local
IMG ?= $(IMAGE_NAME):$(IMAGE_TAG)
CONTAINER_TOOL ?= docker
LDFLAGS := -X github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/version.gitVersion=$(VERSION) \
           -X github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/version.gitCommit=$(COMMIT)

LOCALBIN ?= bin
CONTROLLER_TOOLS_VERSION ?= v0.21.0
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
HELM_VERSION ?= v3.18.5
HELM ?= $(LOCALBIN)/helm
GINKGO_VERSION ?= v2.27.4
GINKGO ?= $(LOCALBIN)/ginkgo
KIND_VERSION ?= v0.32.0
KIND ?= $(LOCALBIN)/kind
KUBECTL_VERSION ?= v1.36.1
KUBECTL ?= $(LOCALBIN)/kubectl
KUBECTL_DOWNLOAD_SHA256 ?=
BUILDKIT_VERSION ?= v0.31.2
BUILDCTL ?= $(LOCALBIN)/buildctl
BUILDCTL_DOWNLOAD_SHA256 ?=
BUILDKIT_IMAGE ?= moby/buildkit:$(BUILDKIT_VERSION)-rootless
BUILDKIT_NAMESPACE ?= buildkit
BUILDKIT_NAME ?= buildkitd
KIND_NODE_IMAGE ?= kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5
KIND_CLUSTER_NAME ?= capacity-planner
KIND_API_SERVER_ADDRESS ?= $(if $(filter Darwin,$(shell uname -s)),127.0.0.1,172.17.0.1)
KIND_API_SERVER_PORT ?= 36443
KIND_REGISTRY_IMAGE ?= registry:2
KIND_REGISTRY_LOAD_BALANCER_IP ?=
KIND_GRAFANA_LOAD_BALANCER_IP ?=
KIND_REGISTRY_CERT_DIR ?= $(BINARY_DIR)/kube/registry
KIND_NETWORK ?= kind
LOCAL_REGISTRY_NAMESPACE ?= local-registry
LOCAL_REGISTRY_SERVICE ?= local-registry
LOCAL_REGISTRY_PORT ?= 5000
METALLB_VERSION ?= v0.16.1
METALLB_MANIFEST ?= https://raw.githubusercontent.com/metallb/metallb/$(METALLB_VERSION)/config/manifests/metallb-native.yaml
CURL ?= curl
OPENSSL ?= openssl

# ENVTEST_VERSION is the exact setup-envtest module version. setup-envtest
# is its own module (release-0.24 branch); the controller-runtime root tag
# v0.24.1 does not contain the tools module. Pinned to the immutable
# pseudo-version currently resolvable on release-0.24; no moving branches.
ENVTEST_VERSION ?= v0.24.2-0.20260713111223-0f529e22d5c0

# ENVTEST_K8S_VERSION is the Kubernetes minor for envtest binaries: the
# latest release listed by `setup-envtest list` (1.36.x), verified locally.
ENVTEST_K8S_VERSION ?= 1.36.2

.PHONY: bin build build/capacity-planner clean clean/bin clean/build deploy deploy/capacity-planner deploy/crd deploy/grafana deploy/kube dockerbuild dockerbuild/capacity-planner fmt fmt/go forward forward/grafana generate generate/crd generate/deepcopy lint lint/chart lint/go push push/capacity-planner sync sync/crd test test/container test/deploy test/deploy-kube test/e2e test/envtest test/evaluation test/install test/unit test/verify-install testdeploy testdeploy/capacity-planner testdeploy/metrics-server testdeploy/tidb-operator verify verify/chart
.NOTPARALLEL: deploy test testdeploy

build: build/capacity-planner

build/capacity-planner:
	mkdir -p $(BINARY_DIR)
	go build -ldflags '$(LDFLAGS)' -o $(BINARY) ./cmd

test: test/unit test/evaluation test/envtest test/container test/install test/verify-install test/deploy-kube test/deploy test/e2e

test/unit: $(HELM)
	go test $$(go list ./... | grep -v '/test/evaluation')

# Historical policy evaluation can be run directly or through the test aggregate.
test/evaluation:
	go test -count=1 ./test/evaluation/...
	go run ./test/evaluation/cmd/autoscaling-eval run \
		--experiment "$(EVALUATION_EXPERIMENT)" \
		--output "$(EVALUATION_OUTPUT)"

# Real-cluster e2e. testdeploy installs test-only dependencies, while deploy
# installs capacity-planner and its local observability stack.
E2E_KUBECONFIG ?= $(CURDIR)/kubeconfig
E2E_ADMIN_NAMESPACE ?= tidb-admin
E2E_TIDB_OPERATOR_VERSION ?= v2.2.0-alpha.5
E2E_TIDB_OPERATOR_CHART ?= oci://ghcr.io/pingcap/charts/tidb-operator
E2E_TIDB_OPERATOR_RELEASE ?= tidb-operator
E2E_METRICS_SERVER_CHART ?= $(CURDIR)/test/e2e/testdata/metrics-server
E2E_METRICS_SERVER_VALUES ?= $(E2E_METRICS_SERVER_CHART)/ci/ci-values.yaml
E2E_METRICS_SERVER_RELEASE ?= metrics-server
E2E_SQL_WORKLOAD_IMAGE ?= mysql:8.0.43
E2E_TIER_TEMPLATES_CONFIG ?= $(CURDIR)/test/e2e/testdata/tier-templates.yaml
FOCUS ?=
DEPLOY_RELEASE ?= capacity-planner
DEPLOY_NAMESPACE ?= $(E2E_ADMIN_NAMESPACE)
DEPLOY_TIMEOUT ?= 5m
DEPLOY_QUERY_BIND_ADDRESS ?= :9090
DEPLOY_QUERY_SERVICE ?= $(DEPLOY_RELEASE)-capacity-planner-query
DEPLOY_QUERY_PORT ?= 9090
GRAFANA_CHART ?= oci://ghcr.io/grafana-community/helm-charts/grafana
GRAFANA_CHART_VERSION ?= 12.10.0
GRAFANA_RELEASE ?= capacity-planner-grafana
GRAFANA_NAMESPACE ?= monitoring
GRAFANA_LOAD_BALANCER_POOL ?= kind-grafana
GRAFANA_DASHBOARD ?= $(CURDIR)/hack/grafana-dashboard.json
GRAFANA_LOCAL_PORT ?= 3000
HELM_CHART ?= charts/capacity-planner

testdeploy: testdeploy/metrics-server testdeploy/tidb-operator testdeploy/capacity-planner

testdeploy/metrics-server: $(HELM) $(KUBECTL)
	@test -d "$(E2E_METRICS_SERVER_CHART)" || { echo "metrics-server chart is not readable: $(E2E_METRICS_SERVER_CHART)" >&2; exit 1; }
	@test -r "$(E2E_METRICS_SERVER_VALUES)" || { echo "metrics-server values are not readable: $(E2E_METRICS_SERVER_VALUES)" >&2; exit 1; }
	"$(HELM)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" upgrade --install \
		"$(E2E_METRICS_SERVER_RELEASE)" "$(abspath $(E2E_METRICS_SERVER_CHART))" \
		--namespace "$(E2E_ADMIN_NAMESPACE)" \
		--create-namespace \
		--values "$(abspath $(E2E_METRICS_SERVER_VALUES))" \
		--wait --atomic --timeout 5m
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" wait \
		--for=condition=Available \
		--timeout=5m \
		apiservice/v1beta1.metrics.k8s.io

testdeploy/tidb-operator: $(HELM) $(KUBECTL)
	"$(HELM)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" upgrade --install \
		"$(E2E_TIDB_OPERATOR_RELEASE)" "$(E2E_TIDB_OPERATOR_CHART)" \
		--version "$(E2E_TIDB_OPERATOR_VERSION)" \
		--namespace "$(E2E_ADMIN_NAMESPACE)" \
		--create-namespace \
		--set appendReleaseSuffix=true \
		--set 'operator.extraArgs[0]=--watch-delay-duration=2s' \
		--set 'operator.extraArgs[1]=--allow-empty-old-version' \
		--wait --atomic --timeout 5m
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" wait \
		--for=condition=Established \
		--timeout=5m \
		crd/clusters.core.pingcap.com \
		crd/pdgroups.core.pingcap.com \
		crd/tidbgroups.core.pingcap.com \
		crd/tikvgroups.core.pingcap.com

testdeploy/capacity-planner: $(HELM) $(KUBECTL)
	@test -r "$(E2E_TIER_TEMPLATES_CONFIG)" || { echo "e2e tier templates are not readable: $(E2E_TIER_TEMPLATES_CONFIG)" >&2; exit 1; }
	"$(HELM)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" upgrade \
		"$(DEPLOY_RELEASE)" "$(HELM_CHART)" \
		--namespace "$(DEPLOY_NAMESPACE)" \
		--reuse-values \
		--set-file tierTemplates.config="$(abspath $(E2E_TIER_TEMPLATES_CONFIG))" \
		--wait --atomic --timeout "$(DEPLOY_TIMEOUT)"
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" \
		-n "$(DEPLOY_NAMESPACE)" rollout status \
		--timeout="$(DEPLOY_TIMEOUT)" "deployment/$(DEPLOY_RELEASE)-capacity-planner"

test/e2e: $(GINKGO)
	$(MAKE) --no-print-directory deploy
	$(MAKE) --no-print-directory testdeploy
	@test -r "$(E2E_KUBECONFIG)" || { echo "e2e kubeconfig is not readable: $(E2E_KUBECONFIG)" >&2; exit 1; }
	KUBECONFIG="$(abspath $(E2E_KUBECONFIG))" \
	GINKGO="$(GINKGO)" \
	ADMIN_NAMESPACE="$(E2E_ADMIN_NAMESPACE)" \
	TIDB_OPERATOR_RELEASE="$(E2E_TIDB_OPERATOR_RELEASE)" \
	CAPACITY_PLANNER_NAMESPACE="$(DEPLOY_NAMESPACE)" \
	CAPACITY_PLANNER_DEPLOYMENT="$(DEPLOY_RELEASE)-capacity-planner" \
	SQL_WORKLOAD_IMAGE="$(E2E_SQL_WORKLOAD_IMAGE)" \
	FOCUS="$(FOCUS)" \
	bash hack/run-e2e.sh

# fmt formats all packages; the dir list comes from `go list` so every
# stacked-PR layer (cmd/ only, api/, internal/, test/) stays valid.
fmt: fmt/go

fmt/go:
	gofmt -w $$(go list -f '{{.Dir}}' ./...)

lint: lint/go lint/chart

lint/go:
	@files="$$(gofmt -l $$(go list -f '{{.Dir}}' ./...))"; \
	if [ -n "$$files" ]; then echo "gofmt needed:"; echo "$$files"; exit 1; fi
	go vet ./...

# DeepCopy / CRD generation (used from PR2 onward). float64 policy fields
# follow the design's Go types; crd:allowDangerousTypes=true is required to
# emit them in the CRD schema.
generate: generate/deepcopy generate/crd

generate/deepcopy: $(CONTROLLER_GEN)
	$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."

generate/crd: $(CONTROLLER_GEN)
	$(CONTROLLER_GEN) crd:allowDangerousTypes=true paths="./..." output:crd:artifacts:config=config/crd/bases

bin: $(CONTROLLER_GEN) $(ENVTEST) $(HELM) $(GINKGO) $(KIND) $(KUBECTL) $(BUILDCTL)

$(CONTROLLER_GEN):
	mkdir -p "$(LOCALBIN)"
	GOBIN="$(abspath $(LOCALBIN))" go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
	@echo "controller-gen pinned at $(CONTROLLER_TOOLS_VERSION)"

$(ENVTEST):
	mkdir -p "$(LOCALBIN)"
	GOBIN="$(abspath $(LOCALBIN))" go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(ENVTEST_VERSION)
	@echo "setup-envtest pinned at $(ENVTEST_VERSION)"

$(HELM):
	mkdir -p "$(LOCALBIN)"
	GOBIN="$(abspath $(LOCALBIN))" go install helm.sh/helm/v3/cmd/helm@$(HELM_VERSION)
	@echo "helm pinned at $(HELM_VERSION)"

$(GINKGO):
	mkdir -p "$(LOCALBIN)"
	GOBIN="$(abspath $(LOCALBIN))" go install github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION)
	@echo "ginkgo pinned at $(GINKGO_VERSION)"

$(KIND):
	mkdir -p "$(LOCALBIN)"
	GOBIN="$(abspath $(LOCALBIN))" go install sigs.k8s.io/kind@$(KIND_VERSION)
	@echo "kind pinned at $(KIND_VERSION)"

$(KUBECTL):
	mkdir -p "$(LOCALBIN)"
	KUBECTL_VERSION="$(KUBECTL_VERSION)" \
	KUBECTL_DEST="$(abspath $(KUBECTL))" \
	KUBECTL_DOWNLOAD_SHA256="$(KUBECTL_DOWNLOAD_SHA256)" \
	CURL="$(CURL)" \
	hack/install-kubectl.sh

$(BUILDCTL):
	mkdir -p "$(LOCALBIN)"
	BUILDKIT_VERSION="$(BUILDKIT_VERSION)" \
	BUILDCTL_DEST="$(abspath $(BUILDCTL))" \
	BUILDCTL_DOWNLOAD_SHA256="$(BUILDCTL_DOWNLOAD_SHA256)" \
	CURL="$(CURL)" \
	hack/install-buildctl.sh

# Persistent four-node Kind cluster for local development. The registry runs
# inside the cluster behind a MetalLB LoadBalancer without a NodePort.
deploy: deploy/kube deploy/crd deploy/capacity-planner deploy/grafana

deploy/kube: $(KIND) $(KUBECTL)
	KIND="$(KIND)" \
	KUBECTL="$(KUBECTL)" \
	CONTAINER_TOOL="$(CONTAINER_TOOL)" \
	OPENSSL="$(OPENSSL)" \
	KIND_NODE_IMAGE="$(KIND_NODE_IMAGE)" \
	KIND_CLUSTER_NAME="$(KIND_CLUSTER_NAME)" \
	KIND_API_SERVER_ADDRESS="$(KIND_API_SERVER_ADDRESS)" \
	KIND_API_SERVER_PORT="$(KIND_API_SERVER_PORT)" \
	KIND_REGISTRY_IMAGE="$(KIND_REGISTRY_IMAGE)" \
	KIND_REGISTRY_LOAD_BALANCER_IP="$(KIND_REGISTRY_LOAD_BALANCER_IP)" \
	KIND_GRAFANA_LOAD_BALANCER_IP="$(KIND_GRAFANA_LOAD_BALANCER_IP)" \
	KIND_REGISTRY_CERT_DIR="$(abspath $(KIND_REGISTRY_CERT_DIR))" \
	KIND_NETWORK="$(KIND_NETWORK)" \
	BUILDKIT_IMAGE="$(BUILDKIT_IMAGE)" \
	BUILDKIT_NAMESPACE="$(BUILDKIT_NAMESPACE)" \
	BUILDKIT_NAME="$(BUILDKIT_NAME)" \
	METALLB_MANIFEST="$(METALLB_MANIFEST)" \
	KUBECONFIG="$(abspath $(E2E_KUBECONFIG))" \
	hack/kube.sh

test/deploy-kube:
	hack/test-kube.sh

# Send the local source tree to rootless buildkitd inside Kind and have
# buildkitd push the resulting image directly to the TLS registry. No image
# archive or layer data returns to the host.
push: push/capacity-planner

push/capacity-planner: $(BUILDCTL) $(KUBECTL)
	@test -r "$(E2E_KUBECONFIG)" || { echo "Kind kubeconfig is not readable: $(E2E_KUBECONFIG); run make deploy/kube first" >&2; exit 1; }
	@set -e; \
	kubectl_path="$$(command -v "$(KUBECTL)")"; \
	[ -n "$$kubectl_path" ] || { echo "kubectl is not executable: $(KUBECTL)" >&2; exit 1; }; \
	kubectl_path="$$(cd "$$(dirname "$$kubectl_path")" && pwd)/$$(basename "$$kubectl_path")"; \
	kubectl_helper_dir="$$(mktemp -d "$${TMPDIR:-/tmp}/capacity-planner-kubectl.XXXXXX")"; \
	trap 'rm -rf "$$kubectl_helper_dir"' EXIT; \
	ln -s "$$kubectl_path" "$$kubectl_helper_dir/kubectl"; \
	if ! registry_ip="$$("$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" \
		-n "$(LOCAL_REGISTRY_NAMESPACE)" get service "$(LOCAL_REGISTRY_SERVICE)" \
		-o 'jsonpath={.status.loadBalancer.ingress[0].ip}')"; then \
		echo "failed to query the local registry; check the Kind API endpoint and kubeconfig" >&2; \
		exit 1; \
	fi; \
	[ -n "$$registry_ip" ] || { echo "local registry has no LoadBalancer IP; run make deploy/kube first" >&2; exit 1; }; \
	if ! buildkit_pod="$$("$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" -n "$(BUILDKIT_NAMESPACE)" get pod -l "app.kubernetes.io/name=$(BUILDKIT_NAME)" -o 'jsonpath={.items[0].metadata.name}')"; then \
		echo "failed to query buildkitd; check the Kind API endpoint and kubeconfig" >&2; \
		exit 1; \
	fi; \
	[ -n "$$buildkit_pod" ] || { echo "no buildkitd Pod found; run make deploy/kube first" >&2; exit 1; }; \
	image="$$registry_ip:$(LOCAL_REGISTRY_PORT)/$(IMAGE_NAME):$(IMAGE_TAG)"; \
	buildkit_addr="kube-pod://$$buildkit_pod?context=kind-$(KIND_CLUSTER_NAME)&namespace=$(BUILDKIT_NAMESPACE)&container=$(BUILDKIT_NAME)"; \
	echo "push/capacity-planner: building and pushing $$image with $(BUILDKIT_NAMESPACE)/$$buildkit_pod"; \
	PATH="$$kubectl_helper_dir:$${PATH}" \
	KUBECONFIG="$(abspath $(E2E_KUBECONFIG))" "$(BUILDCTL)" \
		--addr "$$buildkit_addr" build \
		--progress plain \
		--frontend dockerfile.v0 \
		--local context="$(CURDIR)" \
		--local dockerfile="$(CURDIR)" \
		--opt filename=Dockerfile \
		--opt build-arg:GIT_VERSION="$(VERSION)" \
		--opt build-arg:GIT_COMMIT="$(COMMIT)" \
		--output "type=image,name=$$image,push=true"; \
	echo "push/capacity-planner: pushed $$image"

deploy/crd: $(KUBECTL)
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" apply \
		-f "$(CURDIR)/config/crd/bases/autoscaling.pingcap.com_autoscalers.yaml"
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" wait \
		--for=condition=Established --timeout="$(DEPLOY_TIMEOUT)" \
		crd/autoscalers.autoscaling.pingcap.com

deploy/capacity-planner: deploy/crd push/capacity-planner $(HELM) $(KUBECTL)
	@set -e; \
	if ! registry_ip="$$("$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" \
		-n "$(LOCAL_REGISTRY_NAMESPACE)" get service "$(LOCAL_REGISTRY_SERVICE)" \
		-o 'jsonpath={.status.loadBalancer.ingress[0].ip}')"; then \
		echo "failed to query the local registry; check the Kind API endpoint and kubeconfig" >&2; \
		exit 1; \
	fi; \
	[ -n "$$registry_ip" ] || { echo "local registry has no LoadBalancer IP; run make deploy/kube first" >&2; exit 1; }; \
	image_repository="$${registry_ip}:$(LOCAL_REGISTRY_PORT)/$(IMAGE_NAME)"; \
	"$(HELM)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" upgrade --install \
		"$(DEPLOY_RELEASE)" "$(HELM_CHART)" \
		--namespace "$(DEPLOY_NAMESPACE)" --create-namespace \
		--set-string image.repository="$$image_repository" \
		--set-string image.tag="$(IMAGE_TAG)" \
		--set image.pullPolicy=Always \
		--set-string config.query.bindAddress="$(DEPLOY_QUERY_BIND_ADDRESS)" \
		--wait --atomic --timeout "$(DEPLOY_TIMEOUT)"; \
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" \
		-n "$(DEPLOY_NAMESPACE)" rollout status \
		--timeout="$(DEPLOY_TIMEOUT)" "deployment/$(DEPLOY_RELEASE)-capacity-planner"; \
	echo "deploy/capacity-planner: $(DEPLOY_NAMESPACE)/$(DEPLOY_RELEASE)-capacity-planner uses $${image_repository}:$(IMAGE_TAG)"

deploy/grafana: $(HELM) $(KUBECTL)
	HELM="$(HELM)" \
	KUBECTL="$(KUBECTL)" \
	KUBECONFIG="$(abspath $(E2E_KUBECONFIG))" \
	PLANNER_NAMESPACE="$(DEPLOY_NAMESPACE)" \
	PLANNER_DEPLOYMENT="$(DEPLOY_RELEASE)-capacity-planner" \
	QUERY_BIND_ADDRESS="$(DEPLOY_QUERY_BIND_ADDRESS)" \
	QUERY_SERVICE="$(DEPLOY_QUERY_SERVICE)" \
	QUERY_PORT="$(DEPLOY_QUERY_PORT)" \
	GRAFANA_CHART="$(GRAFANA_CHART)" \
	GRAFANA_CHART_VERSION="$(GRAFANA_CHART_VERSION)" \
	GRAFANA_RELEASE="$(GRAFANA_RELEASE)" \
	GRAFANA_NAMESPACE="$(GRAFANA_NAMESPACE)" \
	GRAFANA_LOAD_BALANCER_POOL="$(GRAFANA_LOAD_BALANCER_POOL)" \
	GRAFANA_DASHBOARD="$(abspath $(GRAFANA_DASHBOARD))" \
	DEPLOY_TIMEOUT="$(DEPLOY_TIMEOUT)" \
	hack/deploy-grafana.sh

forward: forward/grafana

forward/grafana: $(KUBECTL)
	@service="$$($(KUBECTL) --kubeconfig "$(abspath $(E2E_KUBECONFIG))" \
		-n "$(GRAFANA_NAMESPACE)" get service \
		-l "app.kubernetes.io/instance=$(GRAFANA_RELEASE),app.kubernetes.io/name=grafana" \
		-o 'jsonpath={.items[0].metadata.name}')"; \
	[ -n "$$service" ] || { echo "Grafana Service not found; run make deploy first" >&2; exit 1; }; \
	echo "grafana: http://127.0.0.1:$(GRAFANA_LOCAL_PORT)"; \
	"$(KUBECTL)" --kubeconfig "$(abspath $(E2E_KUBECONFIG))" \
		-n "$(GRAFANA_NAMESPACE)" port-forward "service/$$service" \
		"$(GRAFANA_LOCAL_PORT):80"

test/deploy:
	hack/test-deploy.sh

# Sync the chart CRD from the controller-gen output (explicit; verify only
# checks consistency and never edits files).
sync: sync/crd

sync/crd: generate/crd
	cp config/crd/bases/autoscaling.pingcap.com_autoscalers.yaml $(HELM_CHART)/crds/autoscaling.pingcap.com_autoscalers.yaml
	@echo "chart CRD synced from config/crd/bases"

lint/chart: $(HELM)
	"$(HELM)" lint $(HELM_CHART) --strict

# Aggregate chart gate: manifests (CRD source of truth) -> lint -> render ->
# verify script (CRD byte-equality + safety assertions).
verify: verify/chart

verify/chart: generate/crd lint/chart
	"$(HELM)" template $(HELM_CHART) --include-crds > /dev/null
	hack/verify-helm.sh

# Container image: build only, never push. test/container builds a unique
# per-PID temporary tag, asserts user/entrypoint, runs a read-only,
# no-network --help smoke, and removes the temporary tag (also on failure).
dockerbuild: dockerbuild/capacity-planner

dockerbuild/capacity-planner:
	"$(CONTAINER_TOOL)" build -t "$(IMG)" \
		--build-arg GIT_VERSION="$(VERSION)" \
		--build-arg GIT_COMMIT="$(COMMIT)" \
		-f Dockerfile .

test/container:
	hack/verify-image.sh

# Sentinel regression: verify-install.sh must not touch the ambient
# kubeconfig and must not invoke Helm unless the cluster was created.
test/verify-install:
	hack/test-verify-install.sh

# Ephemeral single-node Kind install smoke: build/load/install/wait/
# cleanup of a unique per-PID cluster+release+namespace+image tag.
test/install: $(KIND) $(HELM)
	KIND="$(KIND)" HELM="$(HELM)" CONTAINER_TOOL="$(CONTAINER_TOOL)" \
	KIND_NODE_IMAGE="$(KIND_NODE_IMAGE)" VERSION="$(VERSION)" COMMIT="$(COMMIT)" \
	hack/verify-install.sh

# envtest-backed integration tests (used from PR2 onward). Runs the whole
# integration package: test names are per-behavior (TestValidAutoscalerAccepted
# etc.), not suffixed with "Integration".
test/envtest: $(ENVTEST)
	@if [ ! -d test/integration ]; then \
		echo "no integration suite in this stack layer; skipping"; \
	else \
		assets="$$("$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(abspath $(LOCALBIN))" -p path)" || exit $$?; \
		[ -n "$$assets" ] || { echo "setup-envtest returned no assets for $(ENVTEST_K8S_VERSION)" >&2; exit 1; }; \
		KUBEBUILDER_ASSETS="$$assets" go test -v ./test/integration/...; \
	fi

clean: clean/build clean/bin

clean/build:
	rm -rf "$(BINARY_DIR)"

clean/bin:
	rm -rf "$(LOCALBIN)"
