package gateway

import (
	"fmt"
	"strings"
	"testing"
)

func TestParseRouteConfig(t *testing.T) {
	raw := []byte(`{
  "apiVersion":"gateway.nexuscrm.io/v1",
  "revision":"release-42",
  "routes":[{
    "name":"bounded-read",
    "path":"/api/v1/items/access",
    "methods":["POST"],
    "upstream":"https://app.example.test",
    "timeoutMillis":5000,
    "maxRequestBytes":65536
  }]
}`)
	config, err := ParseRouteConfig(raw)
	if err != nil {
		t.Fatal(err)
	}
	if config.Digest != "sha256:910ba9ff4076f72a9ebd0cb0fa401e775dd424748563e5a16caf8a1ee1c48a63" {
		t.Fatalf("digest = %q", config.Digest)
	}
	if got := config.Routes[0].allowHeader(); got != "POST" {
		t.Fatalf("allow = %q", got)
	}
}

func TestParseRouteConfigV2AcceptsOneOpaqueTerminalSegment(t *testing.T) {
	raw := []byte(`{
  "apiVersion":"gateway.nexuscrm.io/v2",
  "revision":"release-43",
  "routes":[{
    "name":"bounded-page",
    "path":"/api/v1/items/:locator",
    "methods":["GET"],
    "upstream":"https://app.example.test",
    "timeoutMillis":5000,
    "maxRequestBytes":65536
  }]
}`)
	config, err := ParseRouteConfig(raw)
	if err != nil {
		t.Fatal(err)
	}
	if config.APIVersion != RouteConfigAPIVersionV2 || config.Routes[0].Path != "/api/v1/items/:locator" {
		t.Fatalf("config = %#v", config)
	}
}

func TestParseRouteConfigV3AcceptsMultipleOpaqueSegments(t *testing.T) {
	raw := []byte(`{
  "apiVersion":"gateway.nexuscrm.io/v3",
  "revision":"release-44",
  "routes":[{
    "name":"bounded-record",
    "path":"/api/v1/items/:object/:id",
    "methods":["PATCH"],
    "upstream":"https://app.example.test",
    "timeoutMillis":5000,
    "maxRequestBytes":65536
  }]
}`)
	config, err := ParseRouteConfig(raw)
	if err != nil {
		t.Fatal(err)
	}
	path, err := parseRoutePath(config.APIVersion, config.Routes[0].Path)
	if err != nil {
		t.Fatal(err)
	}
	if config.APIVersion != RouteConfigAPIVersionV3 ||
		!path.matches("/api/v1/items/Account/001xx") ||
		path.matches("/api/v1/items/Account") ||
		path.matches("/api/v1/items/Account/001xx/extra") {
		t.Fatalf("v3 path = %#v", path)
	}
}

func TestParseRouteConfigV1KeepsColonLeadingSegmentsExact(t *testing.T) {
	raw := []byte(configJSON(RouteConfigAPIVersion, "release-42", `{"name":"literal","path":"/api/v1/:literal","methods":["GET"],"upstream":"https://app.example.test","timeoutMillis":5000,"maxRequestBytes":65536}`))
	config, err := ParseRouteConfig(raw)
	if err != nil {
		t.Fatal(err)
	}
	path, err := parseRoutePath(config.APIVersion, config.Routes[0].Path)
	if err != nil {
		t.Fatal(err)
	}
	if path.template || path.exact != "/api/v1/:literal" {
		t.Fatalf("v1 path = %#v, want exact colon-leading segment", path)
	}
}

func TestParseRouteConfigRejectsAmbiguousOrUnsafeArtifacts(t *testing.T) {
	validRoute := `{"name":"read","path":"/api/v1/read","methods":["POST"],"upstream":"https://app.example.test","timeoutMillis":5000,"maxRequestBytes":1024}`
	tests := []struct {
		name string
		raw  string
		want string
	}{
		{name: "unknown version", raw: configJSON("v2", "r1", validRoute), want: "unsupported"},
		{name: "unknown field", raw: strings.TrimSuffix(configJSON(RouteConfigAPIVersion, "r1", validRoute), "}") + `,"fallback":"legacy"}`, want: "unknown field"},
		{name: "trailing value", raw: configJSON(RouteConfigAPIVersion, "r1", validRoute) + `{}`, want: "trailing"},
		{name: "duplicate top-level member", raw: `{"apiVersion":"gateway.nexuscrm.io/v1","apiVersion":"gateway.nexuscrm.io/v2","revision":"r1","routes":[` + validRoute + `]}`, want: "duplicate JSON member"},
		{name: "duplicate nested member", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `"upstream":"https://app.example.test"`, `"upstream":"https://reviewed.example.test","upstream":"https://app.example.test"`, 1)), want: "duplicate JSON member"},
		{name: "duplicate path", raw: configJSON(RouteConfigAPIVersion, "r1", validRoute+`,`+strings.Replace(validRoute, `"name":"read"`, `"name":"other"`, 1)), want: "overlaps"},
		{name: "wildcard", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/*`, 1)), want: "canonical"},
		{name: "root template", raw: configJSON(RouteConfigAPIVersionV2, "r1", strings.Replace(validRoute, `/api/v1/read`, `/:id`, 1)), want: "fixed segment"},
		{name: "nonterminal template", raw: configJSON(RouteConfigAPIVersionV2, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/:id/read`, 1)), want: "canonical terminal parameter"},
		{name: "invalid parameter", raw: configJSON(RouteConfigAPIVersionV2, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/v1/:123`, 1)), want: "canonical terminal parameter"},
		{name: "v3 invalid parameter", raw: configJSON(RouteConfigAPIVersionV3, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/:123/read`, 1)), want: "non-canonical parameter"},
		{name: "v3 duplicate parameter name", raw: configJSON(RouteConfigAPIVersionV3, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/:id/:id`, 1)), want: "unique"},
		{name: "v3 all-parameter template", raw: configJSON(RouteConfigAPIVersionV3, "r1", strings.Replace(validRoute, `/api/v1/read`, `/:object/:id`, 1)), want: "fixed segment"},
		{name: "v3 overlapping shapes", raw: configJSON(RouteConfigAPIVersionV3, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/:object/:id`, 1)+`,`+strings.Replace(strings.Replace(validRoute, `"name":"read"`, `"name":"other"`, 1), `/api/v1/read`, `/api/:kind/:key`, 1)), want: "overlaps"},
		{name: "v3 exact overlaps shape", raw: configJSON(RouteConfigAPIVersionV3, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/:object/:id`, 1)+`,`+strings.Replace(strings.Replace(validRoute, `"name":"read"`, `"name":"other"`, 1), `/api/v1/read`, `/api/Account/001`, 1)), want: "overlaps"},
		{name: "duplicate template", raw: configJSON(RouteConfigAPIVersionV2, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/v1/:id`, 1)+`,`+strings.Replace(strings.Replace(validRoute, `"name":"read"`, `"name":"other"`, 1), `/api/v1/read`, `/api/v1/:locator`, 1)), want: "overlaps"},
		{name: "exact overlaps template", raw: configJSON(RouteConfigAPIVersionV2, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/v1/:id`, 1)+`,`+strings.Replace(strings.Replace(validRoute, `"name":"read"`, `"name":"other"`, 1), `/api/v1/read`, `/api/v1/value`, 1)), want: "overlaps"},
		{name: "relative segment", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `/api/v1/read`, `/api/../read`, 1)), want: "relative segment"},
		{name: "operational collision", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `/api/v1/read`, `/readyz`, 1)), want: "operational"},
		{name: "unsupported upstream scheme", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `https://app.example.test`, `file://app.example.test`, 1)), want: "http or https"},
		{name: "upstream path", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `https://app.example.test`, `https://app.example.test/base`, 1)), want: "without path"},
		{name: "upstream trailing slash", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `https://app.example.test`, `https://app.example.test/`, 1)), want: "without path"},
		{name: "upstream whitespace", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `https://app.example.test`, ` https://app.example.test`, 1)), want: "canonical"},
		{name: "duplicate method", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `["POST"]`, `["POST","POST"]`, 1)), want: "duplicate method"},
		{name: "lower method", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `POST`, `post`, 1)), want: "not canonical"},
		{name: "zero timeout", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `5000`, `0`, 1)), want: "timeoutMillis"},
		{name: "zero envelope", raw: configJSON(RouteConfigAPIVersion, "r1", strings.Replace(validRoute, `1024`, `0`, 1)), want: "maxRequestBytes"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := ParseRouteConfig([]byte(test.raw))
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("error = %v, want substring %q", err, test.want)
			}
		})
	}
}

func TestNewRejectsRouteConfigMutationAfterParsing(t *testing.T) {
	config, err := ParseRouteConfig([]byte(configJSON(RouteConfigAPIVersion, "r1", `{"name":"read","path":"/api/v1/read","methods":["POST"],"upstream":"https://app.example.test","timeoutMillis":5000,"maxRequestBytes":1024}`)))
	if err != nil {
		t.Fatal(err)
	}
	config.Revision = "r2"
	if _, err := New(Config{Version: "test", MaxInFlight: 1, Routes: config}); err == nil || !strings.Contains(err.Error(), "unmodified") {
		t.Fatalf("mutated config error = %v", err)
	}
}

func configJSON(version, revision, routes string) string {
	return fmt.Sprintf(`{"apiVersion":%q,"revision":%q,"routes":[%s]}`, version, revision, routes)
}
