// Copyright The Prometheus Authors
// 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.

package v1

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/prometheus/common/promslog"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v2"
)

// TestOpenAPIHTTPHandler verifies that the OpenAPI endpoint serves a valid specification
// with correct headers, structure conforming to OpenAPI 3.1 standards, and consistent responses.
func TestOpenAPIHTTPHandler(t *testing.T) {
	builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())

	// First request.
	req1 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", http.NoBody)
	rec1 := httptest.NewRecorder()
	builder.ServeOpenAPI(rec1, req1)

	// Verify status code and headers.
	require.Equal(t, http.StatusOK, rec1.Code)
	require.True(t, strings.HasPrefix(rec1.Header().Get("Content-Type"), "application/yaml"), "Content-Type should start with application/yaml")
	require.Equal(t, "no-cache, no-store, must-revalidate", rec1.Header().Get("Cache-Control"))

	// Verify it is valid YAML.
	var spec map[string]any
	err := yaml.Unmarshal(rec1.Body.Bytes(), &spec)
	require.NoError(t, err)

	// Verify structure.
	require.Contains(t, spec, "openapi")
	require.Contains(t, spec, "info")
	require.Contains(t, spec, "paths")
	require.Contains(t, spec, "components")

	// Verify OpenAPI version (default is 3.1.0).
	require.Equal(t, "3.1.0", spec["openapi"])

	// Verify info section.
	info, ok := spec["info"].(map[any]any)
	require.True(t, ok, "info should be a map")
	require.Equal(t, "Prometheus API", info["title"])

	// Verify paths exist.
	paths, ok := spec["paths"].(map[any]any)
	require.True(t, ok, "paths should be a map")
	require.NotEmpty(t, paths, "paths should not be empty")

	// Second request to verify response consistency.
	req2 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", http.NoBody)
	rec2 := httptest.NewRecorder()
	builder.ServeOpenAPI(rec2, req2)

	// Both responses should be identical.
	require.Equal(t, rec1.Body.String(), rec2.Body.String())
}

// TestOpenAPIPathFiltering verifies that the IncludePaths option correctly filters
// which API paths are included in the generated specification.
func TestOpenAPIPathFiltering(t *testing.T) {
	tests := []struct {
		name         string
		includePaths []string
		wantPaths    []string
		excludePaths []string
	}{
		{
			name:         "no filter includes all",
			includePaths: nil,
			wantPaths:    []string{"/query", "/labels", "/alerts", "/targets"},
		},
		{
			name:         "filter query paths",
			includePaths: []string{"/query"},
			wantPaths:    []string{"/query", "/query_range", "/query_exemplars"},
			excludePaths: []string{"/labels", "/alerts", "/targets"},
		},
		{
			name:         "filter status paths",
			includePaths: []string{"/status"},
			wantPaths:    []string{"/status/config", "/status/flags", "/status/runtimeinfo"},
			excludePaths: []string{"/query", "/alerts", "/targets"},
		},
		{
			name:         "filter multiple prefixes",
			includePaths: []string{"/label", "/series"},
			wantPaths:    []string{"/labels", "/label/{name}/values", "/series"},
			excludePaths: []string{"/query", "/alerts", "/targets"},
		},
		{
			name:         "exact path match",
			includePaths: []string{"/alerts"},
			wantPaths:    []string{"/alerts"},
			excludePaths: []string{"/alertmanagers", "/query"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			builder := NewOpenAPIBuilder(OpenAPIOptions{
				IncludePaths: tc.includePaths,
			}, promslog.NewNopLogger())

			req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", http.NoBody)
			rec := httptest.NewRecorder()
			builder.ServeOpenAPI(rec, req)

			require.Equal(t, http.StatusOK, rec.Code)

			var spec map[string]any
			err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
			require.NoError(t, err)

			paths, ok := spec["paths"].(map[any]any)
			require.True(t, ok, "paths should be a map")

			for _, want := range tc.wantPaths {
				require.Contains(t, paths, want)
			}

			for _, exclude := range tc.excludePaths {
				require.NotContains(t, paths, exclude)
			}
		})
	}
}

// TestOpenAPISchemaCompleteness verifies that all referenced schemas in paths
// are defined in the components/schemas section of the specification.
func TestOpenAPISchemaCompleteness(t *testing.T) {
	builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())

	req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", http.NoBody)
	rec := httptest.NewRecorder()
	builder.ServeOpenAPI(rec, req)

	var spec map[string]any
	err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
	require.NoError(t, err)

	components, ok := spec["components"].(map[any]any)
	require.True(t, ok, "components should be a map")

	schemas, ok := components["schemas"].(map[any]any)
	require.True(t, ok, "schemas should be a map")

	// Verify essential schemas are present.
	essentialSchemas := []string{
		"Error",
		"Labels",
		"QueryOutputBody",
		"LabelsOutputBody",
		"SeriesOutputBody",
		"TargetsOutputBody",
		"AlertsOutputBody",
		"RulesOutputBody",
		"StatusConfigOutputBody",
		"StatusFlagsOutputBody",
		"PrometheusVersion",
	}

	for _, schema := range essentialSchemas {
		require.Contains(t, schemas, schema)
	}
}

// TODO: Add test to verify all routes from api.go Register() are covered in OpenAPI spec.
// Consider wrapping Router to track registered paths and cross-check with OpenAPI paths.

// TestOpenAPIShouldIncludePath verifies the shouldIncludePath method correctly
// matches paths against the IncludePaths filter configuration.
func TestOpenAPIShouldIncludePath(t *testing.T) {
	tests := []struct {
		name         string
		includePaths []string
		path         string
		expected     bool
	}{
		{
			name:         "empty filter includes all",
			includePaths: nil,
			path:         "/query",
			expected:     true,
		},
		{
			name:         "exact match",
			includePaths: []string{"/query"},
			path:         "/query",
			expected:     true,
		},
		{
			name:         "prefix match",
			includePaths: []string{"/query"},
			path:         "/query_range",
			expected:     true,
		},
		{
			name:         "no match",
			includePaths: []string{"/query"},
			path:         "/labels",
			expected:     false,
		},
		{
			name:         "multiple filters with match",
			includePaths: []string{"/labels", "/series"},
			path:         "/series",
			expected:     true,
		},
		{
			name:         "multiple filters without match",
			includePaths: []string{"/labels", "/series"},
			path:         "/query",
			expected:     false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			builder := &OpenAPIBuilder{
				options: OpenAPIOptions{
					IncludePaths: tc.includePaths,
				},
			}

			result := builder.shouldIncludePath(tc.path)
			require.Equal(t, tc.expected, result)
		})
	}
}

// TestOpenAPIVersionConsistency verifies that both OpenAPI versions are properly generated
// and that 3.2 has exactly one more path than 3.1 (/notifications/live).
func TestOpenAPIVersionConsistency(t *testing.T) {
	builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())

	// Fetch OpenAPI 3.1 spec (default).
	req31 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", http.NoBody)
	rec31 := httptest.NewRecorder()
	builder.ServeOpenAPI(rec31, req31)

	require.Equal(t, http.StatusOK, rec31.Code)

	// Fetch OpenAPI 3.2 spec.
	req32 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml?openapi_version=3.2", http.NoBody)
	rec32 := httptest.NewRecorder()
	builder.ServeOpenAPI(rec32, req32)

	require.Equal(t, http.StatusOK, rec32.Code)

	// Parse both specs.
	var spec31, spec32 map[string]any
	err := yaml.Unmarshal(rec31.Body.Bytes(), &spec31)
	require.NoError(t, err)
	err = yaml.Unmarshal(rec32.Body.Bytes(), &spec32)
	require.NoError(t, err)

	// Verify versions are different.
	require.Equal(t, "3.1.0", spec31["openapi"])
	require.Equal(t, "3.2.0", spec32["openapi"])

	// Verify /notifications/live is only in 3.2.
	paths31 := spec31["paths"].(map[any]any)
	paths32 := spec32["paths"].(map[any]any)

	require.NotContains(t, paths31, "/notifications/live")

	require.Contains(t, paths32, "/notifications/live")

	// Verify 3.2 has exactly one more path than 3.1.
	require.Len(t, paths32, len(paths31)+1,
		"OpenAPI 3.2 should have exactly one more path than 3.1")
}
