/*
 * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
 *
 * 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 modfile

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"golang.org/x/mod/module"
)

var addRequireTests = []struct {
	desc string
	in   string
	path string
	vers string
	out  string
}{
	{
		`existing`,
		`
		module m
		require x.y/z v1.2.3
		`,
		"x.y/z", "v1.5.6",
		`
		module m
		require x.y/z v1.5.6
		`,
	},
	{
		`existing2`,
		`
		module m
		require (
			x.y/z v1.2.3 // first
			x.z/a v0.1.0 // first-a
		)
		require x.y/z v1.4.5 // second
		require (
			x.y/z v1.6.7 // third
			x.z/a v0.2.0 // third-a
		)
		`,
		"x.y/z", "v1.8.9",
		`
		module m

		require (
			x.y/z v1.8.9 // first
			x.z/a v0.1.0 // first-a
		)

		require x.z/a v0.2.0 // third-a
		`,
	},
	{
		`new`,
		`
		module m
		require x.y/z v1.2.3
		`,
		"x.y/w", "v1.5.6",
		`
		module m
		require (
			x.y/z v1.2.3
			x.y/w v1.5.6
		)
		`,
	},
	{
		`new2`,
		`
		module m
		require x.y/z v1.2.3
		require x.y/q/v2 v2.3.4
		`,
		"x.y/w", "v1.5.6",
		`
		module m
		require x.y/z v1.2.3
		require (
			x.y/q/v2 v2.3.4
			x.y/w v1.5.6
		)
		`,
	},
}

type require struct {
	path, vers string
	indirect   bool
}

var setRequireTests = []struct {
	desc string
	in   string
	mods []require
	out  string
}{
	{
		`https://golang.org/issue/45932`,
		`module m
		require (
			x.y/a v1.2.3 //indirect
			x.y/b v1.2.3
			x.y/c v1.2.3
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", false},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3
			x.y/c v1.2.3
		)
		`,
	},
	{
		`existing`,
		`module m
		require (
			x.y/b v1.2.3

			x.y/a v1.2.3
			x.y/d v1.2.3
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", false},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3
			x.y/c v1.2.3
		)
		`,
	},
	{
		`existing_indirect`,
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3 //
			x.y/c v1.2.3 //c
			x.y/d v1.2.3 //   c
			x.y/e v1.2.3 // indirect
			x.y/f v1.2.3 //indirect
			x.y/g v1.2.3 //	indirect
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", true},
			{"x.y/e", "v1.2.3", true},
			{"x.y/f", "v1.2.3", true},
			{"x.y/g", "v1.2.3", true},
		},
		`module m
		require (
			x.y/a v1.2.3 // indirect
			x.y/b v1.2.3 // indirect
			x.y/c v1.2.3 // indirect; c
			x.y/d v1.2.3 // indirect; c
			x.y/e v1.2.3 // indirect
			x.y/f v1.2.3 //indirect
			x.y/g v1.2.3 //	indirect
		)
		`,
	},
	{
		`existing_multi`,
		`module m
		require x.y/a v1.2.3
		require x.y/b v1.2.3
		require x.y/c v1.0.0 // not v1.2.3!
		require x.y/d v1.2.3 // comment kept
		require x.y/e v1.2.3 // comment kept
		require x.y/f v1.2.3 // indirect
		require x.y/g v1.2.3 // indirect
		`,
		[]require{
			{"x.y/h", "v1.2.3", false},
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", false},
			{"x.y/d", "v1.2.3", false},
			{"x.y/e", "v1.2.3", true},
			{"x.y/f", "v1.2.3", false},
			{"x.y/g", "v1.2.3", false},
		},
		`module m
		require x.y/a v1.2.3

		require x.y/b v1.2.3

		require x.y/c v1.2.3 // not v1.2.3!

		require x.y/d v1.2.3 // comment kept

		require x.y/e v1.2.3 // indirect; comment kept

		require x.y/f v1.2.3

		require (
			x.y/g v1.2.3
			x.y/h v1.2.3
		)
		`,
	},
	{
		`existing_duplicate`,
		`module m
		require (
			x.y/a v1.0.0 // zero
			x.y/a v1.1.0 // one
			x.y/a v1.2.3 // two
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
		},
		`module m
		require x.y/a v1.2.3 // indirect; zero
		`,
	},
	{
		`existing_duplicate_multi`,
		`module m
		require x.y/a v1.0.0 // zero
		require x.y/a v1.1.0 // one
		require x.y/a v1.2.3 // two
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
		},
		`module m
		require x.y/a v1.2.3 // indirect; zero
		`,
	},
}

var setRequireSeparateIndirectTests = []struct {
	desc string
	in   string
	mods []require
	out  string
}{
	{
		`https://golang.org/issue/45932`,
		`module m
		require (
			x.y/a v1.2.3 //indirect
			x.y/b v1.2.3
			x.y/c v1.2.3
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", false},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3
			x.y/c v1.2.3
		)
		`,
	},
	{
		`existing`,
		`module m
		require (
			x.y/b v1.2.3

			x.y/a v1.2.3
			x.y/d v1.2.3
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", false},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3
			x.y/c v1.2.3
		)
		`,
	},
	{
		`existing_indirect`,
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3 //
			x.y/c v1.2.3 //c
			x.y/d v1.2.3 //   c
			x.y/e v1.2.3 // indirect
			x.y/f v1.2.3 //indirect
			x.y/g v1.2.3 //	indirect
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", true},
			{"x.y/e", "v1.2.3", true},
			{"x.y/f", "v1.2.3", true},
			{"x.y/g", "v1.2.3", true},
		},
		`module m
		require (
			x.y/a v1.2.3 // indirect
			x.y/b v1.2.3 // indirect
			x.y/c v1.2.3 // indirect; c
			x.y/d v1.2.3 // indirect; c
			x.y/e v1.2.3 // indirect
			x.y/f v1.2.3 //indirect
			x.y/g v1.2.3 //	indirect
		)
		`,
	},
	{
		`existing_line`,
		`module m
		require x.y/a v1.0.0
		require x.y/c v1.0.0 // indirect
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", true},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3
		)
		require (
			x.y/c v1.2.3 // indirect
			x.y/d v1.2.3 // indirect
		)`,
	},
	{
		`existing_multi`,
		`module m
		require x.y/a v1.2.3
		require x.y/b v1.2.3 // demoted to indirect
		require x.y/c v1.0.0 // not v1.2.3!
		require x.y/d v1.2.3 // comment kept
		require x.y/e v1.2.3 // comment kept
		require x.y/f v1.2.3 // indirect; promoted to direct
		// promoted to direct
		require x.y/g v1.2.3 // indirect
		require x.y/i v1.2.3 // indirect
		require x.y/j v1.2.3 // indirect
		`,
		[]require{
			{"x.y/h", "v1.2.3", false}, // out of alphabetical order
			{"x.y/i", "v1.2.3", true},
			{"x.y/j", "v1.2.3", true},
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", false},
			{"x.y/d", "v1.2.3", false},
			{"x.y/e", "v1.2.3", true},
			{"x.y/f", "v1.2.3", false},
			{"x.y/g", "v1.2.3", false},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/h v1.2.3
		)
		require x.y/b v1.2.3 // indirect; demoted to indirect
		require x.y/c v1.2.3 // not v1.2.3!
		require x.y/d v1.2.3 // comment kept
		require x.y/e v1.2.3 // indirect; comment kept
		require x.y/f v1.2.3 // promoted to direct
		// promoted to direct
		require x.y/g v1.2.3
		require x.y/i v1.2.3 // indirect
		require x.y/j v1.2.3 // indirect
		`,
	},
	{
		`existing_duplicate`,
		`module m
		require (
			x.y/a v1.0.0 // zero
			x.y/a v1.1.0 // one
			x.y/a v1.2.3 // two
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
		},
		`module m
		require x.y/a v1.2.3 // indirect; zero
		`,
	},
	{
		`existing_duplicate_multi`,
		`module m
		require x.y/a v1.0.0 // zero
		require x.y/a v1.1.0 // one
		require x.y/a v1.2.3 // two
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
		},
		`module m
		require x.y/a v1.2.3 // indirect; zero
		`,
	},
	{
		`existing_duplicate_mix_indirect`,
		`module m
		require (
			x.y/a v1.0.0 // zero
			x.y/a v1.1.0 // indirect; one
			x.y/a v1.2.3 // indirect; two
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
		},
		`module m
		require x.y/a v1.2.3 // indirect; zero
		`,
	},
	{
		`existing_duplicate_mix_direct`,
		`module m
		require (
			x.y/a v1.0.0 // indirect; zero
			x.y/a v1.1.0 // one
			x.y/a v1.2.3 // two
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
		},
		`module m
		require x.y/a v1.2.3 // zero
		`,
	},
	{
		`add_indirect_after_last_direct`,
		`module m
		require (
			x.y/a v1.0.0 // comment a preserved
			x.y/d v1.0.0 // comment d preserved
		)
		require (
			x.y/b v1.0.0 // comment b preserved
			x.y/e v1.0.0 // comment e preserved
		)
		go 1.17
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", false},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", false},
			{"x.y/e", "v1.2.3", false},
			{"x.y/f", "v1.2.3", true},
		},
		`module m
		require (
			x.y/a v1.2.3 // comment a preserved
			x.y/d v1.2.3 // comment d preserved
		)
		require (
			x.y/b v1.2.3 // comment b preserved
			x.y/e v1.2.3 // comment e preserved
		)
		require (
			x.y/c v1.2.3 // indirect
			x.y/f v1.2.3 // indirect
		)
		go 1.17
		`,
	},
	{
		`add_direct_before_first_indirect`,
		`module m
		require (
			x.y/b v1.0.0 // indirect; comment b preserved
			x.y/e v1.0.0 // indirect; comment d preserved
		)
		require (
			x.y/c v1.0.0 // indirect; comment c preserved
			x.y/f v1.0.0 // indirect; comment e preserved
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", false},
			{"x.y/e", "v1.2.3", true},
			{"x.y/f", "v1.2.3", true},
		},
		`module m
		require (
			x.y/b v1.2.3 // indirect; comment b preserved
			x.y/e v1.2.3 // indirect; comment d preserved
		)
		require (
			x.y/c v1.2.3 // indirect; comment c preserved
			x.y/f v1.2.3 // indirect; comment e preserved
		)
		require (
			x.y/a v1.2.3
			x.y/d v1.2.3
		)
		`,
	},
	{
		`add_indirect_after_mixed`,
		`module m
		require (
			x.y/a v1.0.0
			x.y/b v1.0.0 // indirect
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", false},
			{"x.y/e", "v1.2.3", true},
		},
		`module m
		require (
			x.y/a v1.2.3
			x.y/d v1.2.3
		)
		require (
			x.y/b v1.2.3 // indirect
			x.y/c v1.2.3 // indirect
			x.y/e v1.2.3 // indirect
		)
		`,
	},
	{
		`preserve_block_comment_indirect_to_direct`,
		`module m
		// save
		require (
			x.y/a v1.2.3 // indirect
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", false},
		},
		`module m

		// save
		require x.y/a v1.2.3
		`,
	},
	{
		`preserve_block_comment_direct_to_indirect`,
		`module m
		// save
		require (
			x.y/a v1.2.3
		)
		`,
		[]require{
			{"x.y/a", "v1.2.3", true},
		},
		`module m

		// save
		require x.y/a v1.2.3 // indirect
		`,
	},
	{
		`regroup_flat_uncommented_block`,
		`module m
		require (
			x.y/a v1.0.0 // a
			x.y/b v1.0.0 // indirect; b
			x.y/c v1.0.0 // indirect
		)`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", false},
		},
		`module m
		require (
			x.y/a v1.2.3 // a
			x.y/d v1.2.3
		)
		require (
			x.y/b v1.2.3 // indirect; b
			x.y/c v1.2.3 // indirect
		)`,
	},
	{
		`dont_regroup_flat_commented_block`,
		`module m
		// dont regroup
		require (
			x.y/a v1.0.0
			x.y/b v1.0.0 // indirect
			x.y/c v1.0.0 // indirect
		)`,
		[]require{
			{"x.y/a", "v1.2.3", false},
			{"x.y/b", "v1.2.3", true},
			{"x.y/c", "v1.2.3", true},
			{"x.y/d", "v1.2.3", false},
		},
		`module m
		// dont regroup
		require (
			x.y/a v1.2.3
			x.y/b v1.2.3 // indirect
			x.y/c v1.2.3 // indirect
		)
		require x.y/d v1.2.3`,
	},
}

var addGoTests = []struct {
	desc    string
	in      string
	version string
	out     string
}{
	{
		`module_only`,
		`module m
		`,
		`1.14`,
		`module m
		go 1.14
		`,
	},
	{
		`module_before_require`,
		`module m
		require x.y/a v1.2.3
		`,
		`1.14`,
		`module m
		go 1.14
		require x.y/a v1.2.3
		`,
	},
	{
		`require_before_module`,
		`require x.y/a v1.2.3
		module example.com/inverted
		`,
		`1.14`,
		`require x.y/a v1.2.3
		module example.com/inverted
		go 1.14
		`,
	},
	{
		`require_only`,
		`require x.y/a v1.2.3
		`,
		`1.14`,
		`require x.y/a v1.2.3
		go 1.14
		`,
	},
}

var addGopTests = []struct {
	desc    string
	in      string
	version string
	out     string
}{
	{
		`module_only`,
		`module m
		`,
		`1.0`,
		`module m
		gop 1.0
		`,
	},
	{
		`module_before_require`,
		`module m
		require x.y/a v1.2.3
		`,
		`1.0`,
		`module m
		gop 1.0
		require x.y/a v1.2.3
		`,
	},
	{
		`require_before_module`,
		`require x.y/a v1.2.3
		module example.com/inverted
		`,
		`1.0`,
		`require x.y/a v1.2.3
		module example.com/inverted
		gop 1.0
		`,
	},
	{
		`require_only`,
		`require x.y/a v1.2.3
		`,
		`1.0`,
		`require x.y/a v1.2.3
		gop 1.0
		`,
	},
}

var addExcludeTests = []struct {
	desc    string
	in      string
	path    string
	version string
	out     string
}{
	{
		`compatible`,
		`module m
		`,
		`example.com`,
		`v1.2.3`,
		`module m
		exclude example.com v1.2.3
		`,
	},
	{
		`gopkg.in v0`,
		`module m
		`,
		`gopkg.in/foo.v0`,
		`v0.2.3`,
		`module m
		exclude gopkg.in/foo.v0 v0.2.3
		`,
	},
	{
		`gopkg.in v1`,
		`module m
		`,
		`gopkg.in/foo.v1`,
		`v1.2.3`,
		`module m
		exclude gopkg.in/foo.v1 v1.2.3
		`,
	},
}

var addRetractTests = []struct {
	desc      string
	in        string
	low       string
	high      string
	rationale string
	out       string
}{
	{
		`new_singleton`,
		`module m
		`,
		`v1.2.3`,
		`v1.2.3`,
		``,
		`module m
		retract v1.2.3
		`,
	},
	{
		`new_interval`,
		`module m
		`,
		`v1.0.0`,
		`v1.1.0`,
		``,
		`module m
		retract [v1.0.0, v1.1.0]`,
	},
	{
		`duplicate_with_rationale`,
		`module m
		retract v1.2.3
		`,
		`v1.2.3`,
		`v1.2.3`,
		`bad`,
		`module m
		retract (
			v1.2.3
			// bad
			v1.2.3
		)
		`,
	},
	{
		`duplicate_multiline_rationale`,
		`module m
		retract [v1.2.3, v1.2.3]
		`,
		`v1.2.3`,
		`v1.2.3`,
		`multi
line`,
		`module m
		retract	(
			[v1.2.3, v1.2.3]
			// multi
			// line
			v1.2.3
		)
		`,
	},
	{
		`duplicate_interval`,
		`module m
		retract [v1.0.0, v1.1.0]
		`,
		`v1.0.0`,
		`v1.1.0`,
		``,
		`module m
		retract (
			[v1.0.0, v1.1.0]
			[v1.0.0, v1.1.0]
		)
		`,
	},
	{
		`duplicate_singleton`,
		`module m
		retract v1.2.3
		`,
		`v1.2.3`,
		`v1.2.3`,
		``,
		`module m
		retract	(
			v1.2.3
			v1.2.3
		)
		`,
	},
}

var dropRetractTests = []struct {
	desc string
	in   string
	low  string
	high string
	out  string
}{
	{
		`singleton_no_match`,
		`module m
		retract v1.2.3
		`,
		`v1.0.0`,
		`v1.0.0`,
		`module m
		retract v1.2.3
		`,
	},
	{
		`singleton_match_one`,
		`module m
		retract v1.2.2
		retract v1.2.3
		retract v1.2.4
		`,
		`v1.2.3`,
		`v1.2.3`,
		`module m
		retract v1.2.2
		retract v1.2.4
		`,
	},
	{
		`singleton_match_all`,
		`module m
		retract v1.2.3 // first
		retract v1.2.3 // second
		`,
		`v1.2.3`,
		`v1.2.3`,
		`module m
		`,
	},
	{
		`interval_match`,
		`module m
		retract [v1.2.3, v1.2.3]
		`,
		`v1.2.3`,
		`v1.2.3`,
		`module m
		`,
	},
	{
		`interval_superset_no_match`,
		`module m
		retract [v1.0.0, v1.1.0]
		`,
		`v1.0.0`,
		`v1.2.0`,
		`module m
		retract [v1.0.0, v1.1.0]
		`,
	},
	{
		`singleton_match_middle`,
		`module m
		retract v1.2.3
		`,
		`v1.2.3`,
		`v1.2.3`,
		`module m
		`,
	},
	{
		`interval_match_middle_block`,
		`module m
		retract (
			v1.0.0
			[v1.1.0, v1.2.0]
			v1.3.0
		)
		`,
		`v1.1.0`,
		`v1.2.0`,
		`module m
		retract (
			v1.0.0
			v1.3.0
		)
		`,
	},
	{
		`interval_match_all`,
		`module m
		retract [v1.0.0, v1.1.0]
		retract [v1.0.0, v1.1.0]
		`,
		`v1.0.0`,
		`v1.1.0`,
		`module m
		`,
	},
}

var retractRationaleTests = []struct {
	desc, in, want string
}{
	{
		`no_comment`,
		`module m
		retract v1.0.0`,
		``,
	},
	{
		`prefix_one`,
		`module m
		//   prefix
		retract v1.0.0 
		`,
		`prefix`,
	},
	{
		`prefix_multiline`,
		`module m
		//  one
		//
		//     two
		//
		// three  
		retract v1.0.0`,
		`one

two

three`,
	},
	{
		`suffix`,
		`module m
		retract v1.0.0 // suffix
		`,
		`suffix`,
	},
	{
		`prefix_suffix_after`,
		`module m
		// prefix
		retract v1.0.0 // suffix
		`,
		`prefix
suffix`,
	},
	{
		`block_only`,
		`// block
		retract (
			v1.0.0
		)
		`,
		`block`,
	},
	{
		`block_and_line`,
		`// block
		retract (
			// line
			v1.0.0
		)
		`,
		`line`,
	},
}

var moduleDeprecatedTests = []struct {
	desc, in, want string
}{
	// retractRationaleTests exercises some of the same code, so these tests
	// don't exhaustively cover comment extraction.
	{
		`no_comment`,
		`module m`,
		``,
	},
	{
		`other_comment`,
		`// yo
		module m`,
		``,
	},
	{
		`deprecated_no_colon`,
		`//Deprecated
		module m`,
		``,
	},
	{
		`deprecated_no_space`,
		`//Deprecated:blah
		module m`,
		`blah`,
	},
	{
		`deprecated_simple`,
		`// Deprecated: blah
		module m`,
		`blah`,
	},
	{
		`deprecated_lowercase`,
		`// deprecated: blah
		module m`,
		``,
	},
	{
		`deprecated_multiline`,
		`// Deprecated: one
		// two
		module m`,
		"one\ntwo",
	},
	{
		`deprecated_mixed`,
		`// some other comment
		// Deprecated: blah
		module m`,
		``,
	},
	{
		`deprecated_middle`,
		`// module m is Deprecated: blah
		module m`,
		``,
	},
	{
		`deprecated_multiple`,
		`// Deprecated: a
		// Deprecated: b
		module m`,
		"a\nDeprecated: b",
	},
	{
		`deprecated_paragraph`,
		`// Deprecated: a
		// b
		//
		// c
		module m`,
		"a\nb",
	},
	{
		`deprecated_paragraph_space`,
		`// Deprecated: the next line has a space
		// 
		// c
		module m`,
		"the next line has a space",
	},
	{
		`deprecated_suffix`,
		`module m // Deprecated: blah`,
		`blah`,
	},
	{
		`deprecated_mixed_suffix`,
		`// some other comment
		module m // Deprecated: blah`,
		``,
	},
	{
		`deprecated_mixed_suffix_paragraph`,
		`// some other comment
		//
		module m // Deprecated: blah`,
		`blah`,
	},
	{
		`deprecated_block`,
		`// Deprecated: blah
		module (
			m
		)`,
		`blah`,
	},
}

var sortBlocksTests = []struct {
	desc, in, out string
	strict        bool
}{
	{
		`exclude_duplicates_removed`,
		`module m
		exclude x.y/z v1.0.0 // a
		exclude x.y/z v1.0.0 // b
		exclude (
			x.y/w v1.1.0
			x.y/z v1.0.0 // c
		)
		`,
		`module m
		exclude x.y/z v1.0.0 // a
		exclude (
			x.y/w v1.1.0
		)`,
		true,
	},
	{
		`replace_duplicates_removed`,
		`module m
		replace x.y/z v1.0.0 => ./a
		replace x.y/z v1.1.0 => ./b
		replace (
			x.y/z v1.0.0 => ./c
		)
		`,
		`module m
		replace x.y/z v1.1.0 => ./b
		replace (
			x.y/z v1.0.0 => ./c
		)
		`,
		true,
	},
	{
		`retract_duplicates_not_removed`,
		`module m
		// block
		retract (
			v1.0.0 // one
			v1.0.0 // two
		)`,
		`module m
		// block
		retract (
			v1.0.0 // one
			v1.0.0 // two
		)`,
		true,
	},
	// Tests below this point just check sort order.
	// Non-retract blocks are sorted lexicographically in ascending order.
	// retract blocks are sorted using semver in descending order.
	{
		`sort_lexicographically`,
		`module m
		sort (
			aa
			cc
			bb
			zz
			v1.2.0
			v1.11.0
		)`,
		`module m
		sort (
			aa
			bb
			cc
			v1.11.0
			v1.2.0
			zz
		)
		`,
		false,
	},
	{
		`sort_retract`,
		`module m
		retract (
			[v1.2.0, v1.3.0]
			[v1.1.0, v1.3.0]
			[v1.1.0, v1.2.0]
			v1.0.0
			v1.1.0
			v1.2.0
			v1.3.0
			v1.4.0
		)
		`,
		`module m
		retract (
			v1.4.0
			v1.3.0
			[v1.2.0, v1.3.0]
			v1.2.0
			[v1.1.0, v1.3.0]
			[v1.1.0, v1.2.0]
			v1.1.0
			v1.0.0
		)
		`,
		false,
	},
}

var addRetractValidateVersionTests = []struct {
	desc      string
	path      string
	low, high string
	wantErr   string
}{
	{
		`blank_version`,
		`example.com/m`,
		``,
		``,
		`version "" invalid: must be of the form v1.2.3`,
	},
	{
		`missing prefix`,
		`example.com/m`,
		`1.0.0`,
		`1.0.0`,
		`version "1.0.0" invalid: must be of the form v1.2.3`,
	},
	{
		`non-canonical`,
		`example.com/m`,
		`v1.2`,
		`v1.2`,
		`version "v1.2" invalid: must be of the form v1.2.3`,
	},
	{
		`invalid range`,
		`example.com/m`,
		`v1.2.3`,
		`v1.3`,
		`version "v1.3" invalid: must be of the form v1.2.3`,
	},
	{
		`mismatched major`,
		`example.com/m/v2`,
		`v1.0.0`,
		`v1.0.0`,
		`version "v1.0.0" invalid: should be v2, not v1`,
	},
	{
		`missing +incompatible`,
		`example.com/m`,
		`v2.0.0`,
		`v2.0.0`,
		`version "v2.0.0" invalid: should be v2.0.0+incompatible (or module example.com/m/v2)`,
	},
}

var addExcludeValidateVersionTests = []struct {
	desc    string
	path    string
	version string
	wantErr string
}{
	{
		`blank version`,
		`example.com/m`,
		``,
		`version "" invalid: must be of the form v1.2.3`,
	},
	{
		`missing prefix`,
		`example.com/m`,
		`1.0.0`,
		`version "1.0.0" invalid: must be of the form v1.2.3`,
	},
	{
		`non-canonical`,
		`example.com/m`,
		`v1.2`,
		`version "v1.2" invalid: must be of the form v1.2.3`,
	},
	{
		`mismatched major`,
		`example.com/m/v2`,
		`v1.2.3`,
		`version "v1.2.3" invalid: should be v2, not v1`,
	},
	{
		`missing +incompatible`,
		`example.com/m`,
		`v2.3.4`,
		`version "v2.3.4" invalid: should be v2.3.4+incompatible (or module example.com/m/v2)`,
	},
}

var fixVersionTests = []struct {
	desc, in, want, wantErr string
	fix                     VersionFixer
}{
	{
		desc: `require`,
		in:   `require example.com/m 1.0.0`,
		want: `require example.com/m v1.0.0`,
		fix:  fixV,
	},
	{
		desc: `replace`,
		in:   `replace example.com/m 1.0.0 => example.com/m 1.1.0`,
		want: `replace example.com/m v1.0.0 => example.com/m v1.1.0`,
		fix:  fixV,
	},
	{
		desc: `exclude`,
		in:   `exclude example.com/m 1.0.0`,
		want: `exclude example.com/m v1.0.0`,
		fix:  fixV,
	},
	{
		desc: `retract_single`,
		in: `module example.com/m
		retract 1.0.0`,
		want: `module example.com/m
		retract v1.0.0`,
		fix: fixV,
	},
	{
		desc: `retract_interval`,
		in: `module example.com/m
		retract [1.0.0, 1.1.0]`,
		want: `module example.com/m
		retract [v1.0.0, v1.1.0]`,
		fix: fixV,
	},
	{
		desc:    `retract_nomod`,
		in:      `retract 1.0.0`,
		wantErr: `in:1: no module directive found, so retract cannot be used`,
		fix:     fixV,
	},
}

func fixV(path, version string) (string, error) {
	if path != "example.com/m" {
		return "", fmt.Errorf("module path must be example.com/m")
	}
	return "v" + version, nil
}

func TestAddRequire(t *testing.T) {
	for _, tt := range addRequireTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, true, func(f *File) error {
				err := f.AddRequire(tt.path, tt.vers)
				f.Cleanup()
				return err
			})
		})
	}
}

func TestSetRequire(t *testing.T) {
	for _, tt := range setRequireTests {
		t.Run(tt.desc, func(t *testing.T) {
			var mods []*Require
			for _, mod := range tt.mods {
				mods = append(mods, &Require{
					Mod: module.Version{
						Path:    mod.path,
						Version: mod.vers,
					},
					Indirect: mod.indirect,
				})
			}

			f := testEdit(t, tt.in, tt.out, true, func(f *File) error {
				f.SetRequire(mods)
				f.Cleanup()
				return nil
			})

			if len(f.Require) != len(mods) {
				t.Errorf("after Cleanup, len(Require) = %v; want %v", len(f.Require), len(mods))
			}
		})
	}
}

func TestSetRequireSeparateIndirect(t *testing.T) {
	for _, tt := range setRequireSeparateIndirectTests {
		t.Run(tt.desc, func(t *testing.T) {
			var mods []*Require
			for _, mod := range tt.mods {
				mods = append(mods, &Require{
					Mod: module.Version{
						Path:    mod.path,
						Version: mod.vers,
					},
					Indirect: mod.indirect,
				})
			}

			f := testEdit(t, tt.in, tt.out, true, func(f *File) error {
				f.SetRequireSeparateIndirect(mods)
				f.Cleanup()
				return nil
			})

			if len(f.Require) != len(mods) {
				t.Errorf("after Cleanup, len(Require) = %v; want %v", len(f.Require), len(mods))
			}
		})
	}
}

func TestAddGo(t *testing.T) {
	for _, tt := range addGoTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, true, func(f *File) error {
				return f.AddGoStmt(tt.version)
			})
		})
	}
}

func TestAddGop(t *testing.T) {
	for _, tt := range addGopTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, true, func(f *File) error {
				return f.AddGopStmt(tt.version)
			})
		})
	}
}

func TestAddExclude(t *testing.T) {
	for _, tt := range addExcludeTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, true, func(f *File) error {
				return f.AddExclude(tt.path, tt.version)
			})
		})
	}
}

func TestAddRetract(t *testing.T) {
	for _, tt := range addRetractTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, true, func(f *File) error {
				return f.AddRetract(VersionInterval{Low: tt.low, High: tt.high}, tt.rationale)
			})
		})
	}
}

func TestDropRetract(t *testing.T) {
	for _, tt := range dropRetractTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, true, func(f *File) error {
				if err := f.DropRetract(VersionInterval{Low: tt.low, High: tt.high}); err != nil {
					return err
				}
				f.Cleanup()
				return nil
			})
		})
	}
}

func TestRetractRationale(t *testing.T) {
	for _, tt := range retractRationaleTests {
		t.Run(tt.desc, func(t *testing.T) {
			f, err := Parse("in", []byte(tt.in), nil)
			if err != nil {
				t.Fatal(err)
			}
			if len(f.Retract) != 1 {
				t.Fatalf("got %d retract directives; want 1", len(f.Retract))
			}
			if got := f.Retract[0].Rationale; got != tt.want {
				t.Errorf("got %q; want %q", got, tt.want)
			}
		})
	}
}

func TestModuleDeprecated(t *testing.T) {
	for _, tt := range moduleDeprecatedTests {
		t.Run(tt.desc, func(t *testing.T) {
			f, err := Parse("in", []byte(tt.in), nil)
			if err != nil {
				t.Fatal(err)
			}
			if f.Module.Deprecated != tt.want {
				t.Errorf("got %q; want %q", f.Module.Deprecated, tt.want)
			}
		})
	}
}

func TestSortBlocks(t *testing.T) {
	for _, tt := range sortBlocksTests {
		t.Run(tt.desc, func(t *testing.T) {
			testEdit(t, tt.in, tt.out, tt.strict, func(f *File) error {
				f.SortBlocks()
				return nil
			})
		})
	}
}

func testEdit(t *testing.T, in, want string, strict bool, transform func(f *File) error) *File {
	t.Helper()
	parse := Parse
	if !strict {
		parse = ParseLax
	}
	f, err := parse("in", []byte(in), nil)
	if err != nil {
		t.Fatal(err)
	}
	g, err := parse("out", []byte(want), nil)
	if err != nil {
		t.Fatal(err)
	}
	golden, err := g.Format()
	if err != nil {
		t.Fatal(err)
	}

	if err := transform(f); err != nil {
		t.Fatal(err)
	}
	out, err := f.Format()
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(out, golden) {
		t.Errorf("have:\n%s\nwant:\n%s", out, golden)
	}

	return f
}

func TestAddRetractValidateVersion(t *testing.T) {
	for _, tt := range addRetractValidateVersionTests {
		t.Run(tt.desc, func(t *testing.T) {
			f := new(File)
			if tt.path != "" {
				if err := f.AddModuleStmt(tt.path); err != nil {
					t.Fatal(err)
				}
				t.Logf("module %s", AutoQuote(tt.path))
			}
			interval := VersionInterval{Low: tt.low, High: tt.high}
			if err := f.AddRetract(interval, ``); err == nil || err.Error() != tt.wantErr {
				errStr := "<nil>"
				if err != nil {
					errStr = fmt.Sprintf("%#q", err)
				}
				t.Fatalf("f.AddRetract(%+v, ``) = %s\nwant %#q", interval, errStr, tt.wantErr)
			}
		})
	}
}

func TestAddExcludeValidateVersion(t *testing.T) {
	for _, tt := range addExcludeValidateVersionTests {
		t.Run(tt.desc, func(t *testing.T) {
			f, err := Parse("in", []byte("module m"), nil)
			if err != nil {
				t.Fatal(err)
			}
			if err = f.AddExclude(tt.path, tt.version); err == nil || err.Error() != tt.wantErr {
				errStr := "<nil>"
				if err != nil {
					errStr = fmt.Sprintf("%#q", err)
				}
				t.Fatalf("f.AddExclude(%q, %q) = %s\nwant %#q", tt.path, tt.version, errStr, tt.wantErr)
			}
		})
	}
}

func TestFixVersion(t *testing.T) {
	for _, tt := range fixVersionTests {
		t.Run(tt.desc, func(t *testing.T) {
			inFile, err := Parse("in", []byte(tt.in), tt.fix)
			if err != nil {
				if tt.wantErr == "" {
					t.Fatalf("unexpected error: %v", err)
				}
				if errMsg := err.Error(); !strings.Contains(errMsg, tt.wantErr) {
					t.Fatalf("got error %q; want error containing %q", errMsg, tt.wantErr)
				}
				return
			}
			got, err := inFile.Format()
			if err != nil {
				t.Fatal(err)
			}

			outFile, err := Parse("out", []byte(tt.want), nil)
			if err != nil {
				t.Fatal(err)
			}
			want, err := outFile.Format()
			if err != nil {
				t.Fatal(err)
			}

			if !bytes.Equal(got, want) {
				t.Fatalf("got:\n%s\nwant:\n%s", got, want)
			}
		})
	}
}

var addParseExtTests = []struct {
	desc    string
	ext     string
	want    string
	wantErr string
}{
	{
		"spx ok",
		".spx",
		".spx",
		"",
	},
	{
		"no ext",
		"",
		"",
		"",
	},
	{
		"not a ext",
		"gmx",
		"",
		"ext \"gmx\" invalid: invalid ext format",
	},
}

func TestParseExt(t *testing.T) {
	for _, tt := range addParseExtTests {
		t.Run(tt.desc, func(t *testing.T) {
			ext, err := parseExt(&tt.ext)
			if err != nil {
				if err.Error() != tt.wantErr {
					t.Fatalf("wanterr %s,but got %s", tt.wantErr, err)
				}
			}
			if ext != tt.want {
				t.Fatalf("want %s,but got %s", tt.want, ext)
			}
		})
	}
}

var gopmod = `
module spx

go 1.17
gop 1.1

classfile .gmx .spx github.com/goplus/spx math

require (
    github.com/ajstarks/svgo v0.0.0-20210927141636-6d70534b1098
)
`

var gopmod2 = `
module moduleUserProj

go 1.17
gop 1.1

register github.com/goplus/spx

require (
    github.com/goplus/spx v1.0
)
`

func TestParse(t *testing.T) {
	f, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod), func(path, vers string) (resolved string, err error) {
		return vers, nil
	})
	if err != nil {
		t.Error(err)
		return
	}
	if f.Gop.Version != "1.1" {
		t.Errorf("gop version expected be 1.1, but %s got", f.Gop.Version)
	}

	if f.Classfile.ProjExt != ".gmx" {
		t.Errorf("classfile exts expected be .gmx, but %s got", f.Classfile.ProjExt)
	}
	if f.Classfile.WorkExt != ".spx" {
		t.Errorf("classfile exts expected be .spx, but %s got", f.Classfile.WorkExt)
	}

	if len(f.Classfile.PkgPaths) != 2 {
		t.Errorf("classfile pkgpaths length expected be 2, but %d got", len(f.Classfile.PkgPaths))
	}

	if f.Classfile.PkgPaths[0] != "github.com/goplus/spx" {
		t.Errorf("classfile path expected be github.com/goplus/spx, but %s got", f.Classfile.PkgPaths[0])
	}
	if f.Classfile.PkgPaths[1] != "math" {
		t.Errorf("classfile path expected be math, but %s got", f.Classfile.PkgPaths[1])
	}

	f2, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod2), func(path, vers string) (resolved string, err error) {
		return vers, nil
	})

	if err != nil {
		t.Error(err)
		return
	}

	if f2.Register.ClassfileMod != "github.com/goplus/spx" {
		t.Errorf("register classfile mod expected be github.com/goplus/spx, but %s got", f2.Register.ClassfileMod)
	}
}
