// Copyright 2025 CUE 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 jsonschema_test

import (
	"io"
	"io/fs"
	"maps"
	"slices"
	"testing"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/errors"
	"cuelang.org/go/cue/format"
	"cuelang.org/go/encoding/jsonschema"
	"cuelang.org/go/internal/cuetdtest"
	"cuelang.org/go/internal/cuetxtar"
	"github.com/go-quicktest/qt"
	"golang.org/x/tools/txtar"
)

func TestGenerate(t *testing.T) {
	t.Parallel()
	test := cuetxtar.TxTarTest{
		Root:   "./testdata/generate",
		Name:   "generate",
		Matrix: cuetdtest.SmallMatrix,
	}
	test.Run(t, func(t *cuetxtar.Test) {
		t.Logf("test name %q", t.T.Name())
		ctx := t.CueContext()
		v := ctx.BuildInstance(t.Instance())
		qt.Assert(t, qt.IsNil(v.Err()))
		if p, ok := t.Value("path"); ok {
			v = v.LookupPath(cue.ParsePath(p))
		}
		r, err := jsonschema.Generate(v, &jsonschema.GenerateConfig{
			ExplicitOpen: t.HasTag("explicitOpen"),
		})
		qt.Assert(t, qt.IsNil(err))
		data, err := format.Node(r)
		qt.Assert(t, qt.IsNil(err))
		t.Writer("schema").Write(data)
		t.Logf("generated schema: %q", data)

		// Round-trip test: convert generated JSON Schema back to CUE to validate.
		schemaValue := ctx.BuildExpr(r)
		qt.Assert(t, qt.IsNil(schemaValue.Err()), qt.Commentf("schema data: %q", data))

		schemaBytes, err := schemaValue.MarshalJSON()
		qt.Assert(t, qt.IsNil(err))

		schemaValue = ctx.CompileBytes(schemaBytes)
		qt.Assert(t, qt.IsNil(schemaValue.Err()))

		extractedSchemaFile, err := jsonschema.Extract(schemaValue, &jsonschema.Config{
			StrictFeatures: true,
			StrictKeywords: true,
		})
		if t.HasTag("brokenRoundTrip") {
			t.Skipf("round-trip extraction skipped (brokenRoundTrip)")
			return
		}
		qt.Assert(t, qt.IsNil(err), qt.Commentf("generated JSON Schema should round-trip cleanly via Extract"))
		extractedSchemaValue := ctx.BuildFile(extractedSchemaFile)
		t.Logf("extracted schema: %#v", extractedSchemaValue)
		qt.Assert(t, qt.IsNil(extractedSchemaValue.Err()))

		txfs, err := txtar.FS(t.Archive)
		if err != nil {
			for _, f := range t.Archive.Files {
				t.Logf("- %v", f.Name)
			}
		}
		qt.Assert(t, qt.IsNil(err))
		if _, err := fs.Stat(txfs, "datatest"); err != nil {
			return
		}
		dataTestInst := t.Instances("./datatest")[0]
		dataTestv := ctx.BuildInstance(dataTestInst)
		var tests map[string]*generateDataTest
		err = dataTestv.Decode(&tests)
		qt.Assert(t, qt.IsNil(err))
		cwd := t.Dir
		outert := t
		for _, testName := range slices.Sorted(maps.Keys(tests)) {
			dataTest := tests[testName]
			// TODO it would be nice to be able to run each data test as
			// its own subtest but that's not nicely compatible with the
			// way that cuetxtar works, because either we have one
			// "out" file per test, in which case we'll end up with orphan
			// out files every time a test name changes, or we have one
			// out file for all tests, but that's awkward to manage when the
			// user can run any subset of tests with cue test -run.
			// A better approach might be to avoid cuetxtar completely
			// in favor of something more akin to the external decoder tests,
			// but it's harder to update CUE than JSON, so for now we
			// just avoid running subtests.
			t.Run(testName, func(t *testing.T) {
				qt.Assert(t, qt.IsTrue(dataTest.Data.Exists()))
				qt.Assert(t, qt.IsNil(dataTest.Data.Validate(cue.Concrete(true))))
				rv := dataTest.Data.Unify(extractedSchemaValue)
				err := rv.Validate(cue.Concrete(true))
				w := outert.Writer(testName)
				if dataTest.Error {
					qt.Assert(t, qt.Not(qt.IsNil(err)))
					errStr := errors.Details(err, &errors.Config{
						Cwd:     cwd,
						ToSlash: true,
					})
					io.WriteString(w, errStr)
					return
				}
				qt.Assert(t, qt.IsNil(err))
			})
		}
	})
}

type generateDataTest struct {
	Data  cue.Value `json:"data"`
	Error bool      `json:"error"`
}
