/*
Copyright 2019 The Kubernetes 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 objectmeta

import (
	"bytes"
	"reflect"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"

	"k8s.io/apimachinery/pkg/util/json"

	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)

func TestCoerce(t *testing.T) {
	tests := []struct {
		name                  string
		json                  string
		includeRoot           bool
		dropInvalidFields     bool
		schema                *structuralschema.Structural
		expected              string
		expectedError         bool
		expectedUnknownFields []string
	}{
		{name: "empty", json: "null", schema: nil, expected: "null"},
		{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
		{name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
			Items: &structuralschema.Structural{},
		}, expected: "[1,2]"},
		{name: "x-kubernetes-embedded-resource", json: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance",
    "unspecified": "bar"
  },
  "unspecified":"bar",
  "pruned": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance",
      "unspecified": "bar"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "preserving": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance",
      "unspecified": "bar"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "nested": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance",
      "unspecified": "bar"
    },
    "spec": {
      "unspecified": "bar",
      "embedded": {
        "apiVersion": "foo/v1",
        "kind": "Foo",
        "unspecified": "bar",
        "metadata": {
          "name": "instance",
          "unspecified": "bar"
        },
        "spec": {
          "unspecified": "bar"
        }
      }
    }
  }
}
`, schema: &structuralschema.Structural{
			Generic: structuralschema.Generic{Type: "object"},
			Properties: map[string]structuralschema.Structural{
				"pruned": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
					Properties: map[string]structuralschema.Structural{
						"spec": {
							Generic: structuralschema.Generic{Type: "object"},
						},
					},
				},
				"preserving": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource:      true,
						XPreserveUnknownFields: true,
					},
				},
				"nested": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
					Properties: map[string]structuralschema.Structural{
						"spec": {
							Generic: structuralschema.Generic{Type: "object"},
							Properties: map[string]structuralschema.Structural{
								"embedded": {
									Generic: structuralschema.Generic{Type: "object"},
									Extensions: structuralschema.Extensions{
										XEmbeddedResource: true,
									},
									Properties: map[string]structuralschema.Structural{
										"spec": {
											Generic: structuralschema.Generic{Type: "object"},
										},
									},
								},
							},
						},
					},
				},
			},
		}, expected: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance",
    "unspecified": "bar"
  },
  "unspecified":"bar",
  "pruned": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "preserving": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "nested": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance"
    },
    "spec": {
      "unspecified": "bar",
      "embedded": {
        "apiVersion": "foo/v1",
        "kind": "Foo",
        "unspecified": "bar",
        "metadata": {
          "name": "instance"
        },
        "spec": {
          "unspecified": "bar"
        }
      }
    }
  }
}
`, expectedUnknownFields: []string{
			"nested.metadata.unspecified",
			"nested.spec.embedded.metadata.unspecified",
			"preserving.metadata.unspecified",
			"pruned.metadata.unspecified",
		}},
		{name: "x-kubernetes-embedded-resource, with includeRoot=true", json: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance",
    "unspecified": "bar"
  },
  "unspecified":"bar",
  "pruned": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance",
      "unspecified": "bar"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "preserving": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance",
      "unspecified": "bar"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "nested": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance",
      "unspecified": "bar"
    },
    "spec": {
      "unspecified": "bar",
      "embedded": {
        "apiVersion": "foo/v1",
        "kind": "Foo",
        "unspecified": "bar",
        "metadata": {
          "name": "instance",
          "unspecified": "bar"
        },
        "spec": {
          "unspecified": "bar"
        }
      }
    }
  }
}
`, includeRoot: true, schema: &structuralschema.Structural{
			Generic: structuralschema.Generic{Type: "object"},
			Properties: map[string]structuralschema.Structural{
				"pruned": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
					Properties: map[string]structuralschema.Structural{
						"spec": {
							Generic: structuralschema.Generic{Type: "object"},
						},
					},
				},
				"preserving": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource:      true,
						XPreserveUnknownFields: true,
					},
				},
				"nested": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
					Properties: map[string]structuralschema.Structural{
						"spec": {
							Generic: structuralschema.Generic{Type: "object"},
							Properties: map[string]structuralschema.Structural{
								"embedded": {
									Generic: structuralschema.Generic{Type: "object"},
									Extensions: structuralschema.Extensions{
										XEmbeddedResource: true,
									},
									Properties: map[string]structuralschema.Structural{
										"spec": {
											Generic: structuralschema.Generic{Type: "object"},
										},
									},
								},
							},
						},
					},
				},
			},
		}, expected: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "unspecified":"bar",
  "pruned": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "preserving": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance"
    },
    "spec": {
      "unspecified": "bar"
    }
  },
  "nested": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "unspecified": "bar",
    "metadata": {
      "name": "instance"
    },
    "spec": {
      "unspecified": "bar",
      "embedded": {
        "apiVersion": "foo/v1",
        "kind": "Foo",
        "unspecified": "bar",
        "metadata": {
          "name": "instance"
        },
        "spec": {
          "unspecified": "bar"
        }
      }
    }
  }
}`, expectedUnknownFields: []string{
			"metadata.unspecified",
			"nested.metadata.unspecified",
			"nested.spec.embedded.metadata.unspecified",
			"preserving.metadata.unspecified",
			"pruned.metadata.unspecified",
		}},
		{name: "without name", json: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "pruned": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "metadata": {
      "namespace": "kube-system"
    }
  }
}
`, schema: &structuralschema.Structural{
			Generic: structuralschema.Generic{Type: "object"},
			Properties: map[string]structuralschema.Structural{
				"pruned": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
				},
			},
		}, expected: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "pruned": {
    "apiVersion": "foo/v1",
    "kind": "Foo",
    "metadata": {
      "namespace": "kube-system"
    }
  }
}
`},
		{name: "x-kubernetes-embedded-resource, with dropInvalidFields=true", json: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "pruned": {
    "apiVersion": 42,
    "kind": 42,
    "metadata": {
      "name": "instance",
	  "namespace": ["abc"],
      "labels": {
        "foo": 42
      }
    }
  }
}
`, dropInvalidFields: true, schema: &structuralschema.Structural{
			Generic: structuralschema.Generic{Type: "object"},
			Properties: map[string]structuralschema.Structural{
				"pruned": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
					Properties: map[string]structuralschema.Structural{
						"spec": {
							Generic: structuralschema.Generic{Type: "object"},
						},
					},
				},
			},
		}, expected: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "pruned": {
    "metadata": {
      "name": "instance"
    }
  }
}
`},
		{name: "invalid metadata type, with dropInvalidFields=true", json: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "pruned": {
    "apiVersion": 42,
    "kind": 42,
    "metadata": [42]
  }
}
`, dropInvalidFields: true, schema: &structuralschema.Structural{
			Generic: structuralschema.Generic{Type: "object"},
			Properties: map[string]structuralschema.Structural{
				"pruned": {
					Generic: structuralschema.Generic{Type: "object"},
					Extensions: structuralschema.Extensions{
						XEmbeddedResource: true,
					},
				},
			},
		}, expected: `
{
  "apiVersion": "foo/v1",
  "kind": "Foo",
  "metadata": {
    "name": "instance"
  },
  "pruned": {
    "metadata": [42]
  }
}
`},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var in interface{}
			if err := json.Unmarshal([]byte(tt.json), &in); err != nil {
				t.Fatal(err)
			}

			var expected interface{}
			if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
				t.Fatal(err)
			}

			err, unknownFields := CoerceWithOptions(nil, in, tt.schema, tt.includeRoot, CoerceOptions{
				DropInvalidFields:       tt.dropInvalidFields,
				ReturnUnknownFieldPaths: true,
			})
			if tt.expectedError && err == nil {
				t.Error("expected error, but did not get any")
			} else if !tt.expectedError && err != nil {
				t.Errorf("expected no error, but got: %v", err)
			} else if !reflect.DeepEqual(in, expected) {
				var buf bytes.Buffer
				enc := json.NewEncoder(&buf)
				enc.SetIndent("", "  ")
				err := enc.Encode(in)
				if err != nil {
					t.Fatalf("unexpected result mashalling error: %v", err)
				}
				t.Errorf("expected: %s\ngot: %s\ndiff: %s", tt.expected, buf.String(), cmp.Diff(expected, in))
			}
			if !reflect.DeepEqual(unknownFields, tt.expectedUnknownFields) {
				t.Errorf("expected unknown fields:\n\t%v\ngot:\n\t%v\n", strings.Join(tt.expectedUnknownFields, "\n\t"), strings.Join(unknownFields, "\n\t"))
			}
		})
	}
}
