package jsonio

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestMarshalIndentNoEscape(t *testing.T) {
	v := map[string]string{"sel": `{__name__=~"foo>bar"}`}
	out, err := MarshalIndent(v)
	assert.NoError(t, err)
	s := string(out)
	assert.Contains(t, s, `foo>bar`)
	assert.Contains(t, s, `{__name__=~`)
	for _, bad := range []string{"\\u003c", "\\u003e", "\\u0026"} {
		assert.NotContains(t, s, bad, "HTML-escape artifact should not be present")
	}
	assert.True(t, strings.HasSuffix(s, "\n"), "trailing newline missing")
}

func TestAppendSynced(t *testing.T) {
	path := filepath.Join(t.TempDir(), "jobs.jsonl")
	for _, chunk := range []string{"{\"id\":1}\n", "{\"id\":2}\n", "{\"id\":3}\n"} {
		assert.NoError(t, AppendSynced(path, []byte(chunk)))
	}
	data, err := os.ReadFile(path)
	assert.NoError(t, err)
	assert.Equal(t, "{\"id\":1}\n{\"id\":2}\n{\"id\":3}\n", string(data))
}

func TestAppendSyncedOpenFailure(t *testing.T) {
	dir := t.TempDir()
	// Appending to a path occupied by a directory fails at open.
	assert.Error(t, AppendSynced(dir, []byte("{}\n")))
}

type errWriter struct{ err error }

func (w errWriter) Write([]byte) (int, error) { return 0, w.err }

// shortWriter reports a successful write of only half the buffer.
type shortWriter struct{ written int }

func (w *shortWriter) Write(p []byte) (int, error) {
	w.written = len(p) / 2
	return w.written, nil
}

func TestWriteFull(t *testing.T) {
	t.Run("Full", func(t *testing.T) {
		var sb strings.Builder
		assert.NoError(t, WriteFull(&sb, []byte("abc")))
		assert.Equal(t, "abc", sb.String())
	})
	t.Run("Empty", func(t *testing.T) {
		assert.NoError(t, WriteFull(&shortWriter{}, nil))
	})
	t.Run("ShortWrite", func(t *testing.T) {
		w := &shortWriter{}
		err := WriteFull(w, []byte("abcd"))
		assert.ErrorIs(t, err, io.ErrShortWrite)
		assert.Equal(t, 2, w.written)
	})
	t.Run("WriteError", func(t *testing.T) {
		boom := errors.New("boom")
		assert.ErrorIs(t, WriteFull(errWriter{err: boom}, []byte("abcd")), boom)
	})
}

type doc struct {
	A int    `json:"a"`
	B string `json:"b"`
}

func TestWriteFileAndReadFileRoundTrip(t *testing.T) {
	path := filepath.Join(t.TempDir(), "doc.json")
	in := doc{A: 42, B: `{__name__=~"x>y"}`}
	assert.NoError(t, WriteFile(path, in))

	var out doc
	assert.NoError(t, ReadFile(path, &out))
	assert.Equal(t, in, out)

	data, err := os.ReadFile(path)
	assert.NoError(t, err)
	assert.Contains(t, string(data), "x>y", "WriteFile must not HTML-escape")
}

func TestWriteFileUnmarshalable(t *testing.T) {
	path := filepath.Join(t.TempDir(), "bad.json")
	assert.Error(t, WriteFile(path, map[string]any{"ch": make(chan int)}))
	_, err := os.Stat(path)
	assert.True(t, os.IsNotExist(err), "failed write must not create the file")
}

func TestReadFileMissing(t *testing.T) {
	var out doc
	err := ReadFile(filepath.Join(t.TempDir(), "absent.json"), &out)
	assert.Error(t, err)
}

func TestReadFileBadJSON(t *testing.T) {
	path := filepath.Join(t.TempDir(), "bad.json")
	assert.NoError(t, os.WriteFile(path, []byte("{not json"), 0o600))
	var out doc
	assert.Error(t, ReadFile(path, &out))
}

func TestReadFileStrict(t *testing.T) {
	t.Run("KnownFields", func(t *testing.T) {
		path := filepath.Join(t.TempDir(), "doc.json")
		assert.NoError(t, WriteFile(path, doc{A: 1, B: "x"}))
		var out doc
		assert.NoError(t, ReadFileStrict(path, &out))
		assert.Equal(t, doc{A: 1, B: "x"}, out)
	})
	t.Run("UnknownFieldRejected", func(t *testing.T) {
		path := filepath.Join(t.TempDir(), "doc.json")
		assert.NoError(t, os.WriteFile(path, []byte(`{"a":1,"typo":2}`), 0o600))
		var out doc
		err := ReadFileStrict(path, &out)
		assert.ErrorContains(t, err, "unknown field")
	})
	t.Run("Missing", func(t *testing.T) {
		var out doc
		assert.Error(t, ReadFileStrict(filepath.Join(t.TempDir(), "absent.json"), &out))
	})
}

func TestDecodeStrict(t *testing.T) {
	t.Run("Valid", func(t *testing.T) {
		var out doc
		assert.NoError(t, DecodeStrict(strings.NewReader(`{"a":7}`), &out))
		assert.Equal(t, 7, out.A)
	})
	t.Run("UnknownField", func(t *testing.T) {
		var out doc
		assert.ErrorContains(t, DecodeStrict(strings.NewReader(`{"a":7,"zzz":1}`), &out), "unknown field")
	})
	t.Run("Malformed", func(t *testing.T) {
		var out doc
		assert.Error(t, DecodeStrict(strings.NewReader("{oops"), &out))
	})
}
