// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package runtime

import (
	"bytes"
	"encoding/csv"
	"errors"
	"io"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-openapi/testify/v2/assert"
	"github.com/go-openapi/testify/v2/require"
)

const (
	csvFixture = `name,country,age
John,US,19
Mike,US,20
`
	badCSVFixture = `name,country,age
John,US,19
Mike,US
`
	commentedCSVFixture = `# heading line
name,country,age
#John's record
John,US,19
#Mike's record
Mike,US,20
`
)

var testCSVRecords = [][]string{
	{"name", "country", "age"},
	{"John", "US", "19"},
	{"Mike", "US", "20"},
}

func TestCSVConsumer(t *testing.T) {
	consumer := CSVConsumer()

	t.Run("can consume as a *csv.Writer", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var buf bytes.Buffer
		dest := csv.NewWriter(&buf)

		err := consumer.Consume(reader, dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, buf.String())
	})

	t.Run("can consume as a CSVReader", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var dest csvRecordsWriter

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assertCSVRecords(t, dest.records)
	})

	t.Run("can consume as a Writer", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var dest closingWriter

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, dest.b.String())
	})

	t.Run("can consume as a ReaderFrom", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var dest readerFromDummy

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, dest.b.String())
	})

	t.Run("can consume as a BinaryUnmarshaler", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var dest binaryUnmarshalDummy

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, dest.str)
	})

	t.Run("can consume as a *[][]string", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		dest := [][]string{}

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assertCSVRecords(t, dest)
	})

	t.Run("can consume as an alias to *[][]string", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		type records [][]string
		var dest records

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assertCSVRecords(t, dest)
	})

	t.Run("can consume as a *[]byte", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var dest []byte

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, string(dest))
	})

	t.Run("can consume as an alias to *[]byte", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		type buffer []byte
		var dest buffer

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, string(dest))
	})

	t.Run("can consume as a *string", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		var dest string

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, dest)
	})

	t.Run("can consume as an alias to *string", func(t *testing.T) {
		reader := bytes.NewBufferString(csvFixture)
		type buffer string
		var dest buffer

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, string(dest))
	})

	t.Run("can consume from an empty reader", func(t *testing.T) {
		reader := &csvEmptyReader{}
		var dest bytes.Buffer

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)
		assert.Empty(t, dest.String())
	})

	t.Run("error cases", func(t *testing.T) {
		t.Run("nil data is never accepted", func(t *testing.T) {
			var rdr bytes.Buffer

			require.Error(t, consumer.Consume(&rdr, nil))
		})

		t.Run("nil readers should also never be acccepted", func(t *testing.T) {
			var buf bytes.Buffer

			err := consumer.Consume(nil, &buf)
			require.Error(t, err)
		})

		t.Run("data must be a pointer", func(t *testing.T) {
			var rdr bytes.Buffer
			var dest []byte

			err := consumer.Consume(&rdr, dest)
			require.Error(t, err)
		})

		t.Run("unsupported type", func(t *testing.T) {
			var rdr bytes.Buffer
			var dest struct{}

			err := consumer.Consume(&rdr, &dest)
			require.Error(t, err)
		})

		t.Run("should propagate CSV error (buffered)", func(t *testing.T) {
			reader := bytes.NewBufferString(badCSVFixture)
			var dest []byte

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "record on line 3: wrong number of fields")
		})

		t.Run("should propagate CSV error (buffered, string)", func(t *testing.T) {
			reader := bytes.NewBufferString(badCSVFixture)
			var dest string

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "record on line 3: wrong number of fields")
		})

		t.Run("should propagate CSV error (buffered, ReaderFrom)", func(t *testing.T) {
			reader := bytes.NewBufferString(badCSVFixture)
			var dest readerFromDummy

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "record on line 3: wrong number of fields")
		})

		t.Run("should propagate CSV error (buffered, BinaryUnmarshaler)", func(t *testing.T) {
			reader := bytes.NewBufferString(badCSVFixture)
			var dest binaryUnmarshalDummy

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "record on line 3: wrong number of fields")
		})

		t.Run("should propagate CSV error (streaming)", func(t *testing.T) {
			reader := bytes.NewBufferString(badCSVFixture)
			var dest bytes.Buffer

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "record on line 3: wrong number of fields")
		})

		t.Run("should propagate CSV error (streaming, write error)", func(t *testing.T) {
			reader := bytes.NewBufferString(csvFixture)
			var buf bytes.Buffer
			dest := csvWriterDummy{err: errors.New("test error"), Writer: csv.NewWriter(&buf)}

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "test error")
		})

		t.Run("should propagate ReaderFrom error", func(t *testing.T) {
			reader := bytes.NewBufferString(csvFixture)
			dest := readerFromDummy{err: errors.New("test error")}

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "test error")
		})

		t.Run("should propagate BinaryUnmarshaler error", func(t *testing.T) {
			reader := bytes.NewBufferString(csvFixture)
			dest := binaryUnmarshalDummy{err: errors.New("test error")}

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "test error")
		})
	})
}

func TestCSVConsumerWithOptions(t *testing.T) {
	semiColonFixture := strings.ReplaceAll(csvFixture, ",", ";")

	t.Run("with CSV reader Comma", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVReaderOpts(csv.Reader{Comma: ';', FieldsPerRecord: 3}))

		t.Run("should not read comma-separated input", func(t *testing.T) {
			reader := bytes.NewBufferString(csvFixture)
			var dest bytes.Buffer

			err := consumer.Consume(reader, &dest)
			require.Error(t, err)
			require.EqualError(t, err, "record on line 1: wrong number of fields")
		})

		t.Run("should read semicolon-separated input and convert it to colon-separated", func(t *testing.T) {
			reader := bytes.NewBufferString(semiColonFixture)
			var dest bytes.Buffer

			err := consumer.Consume(reader, &dest)
			require.NoError(t, err)
			assert.EqualT(t, csvFixture, dest.String())
		})
	})

	t.Run("with CSV reader Comment", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVReaderOpts(csv.Reader{Comment: '#'}))

		t.Run("should read input and skip commented lines", func(t *testing.T) {
			reader := bytes.NewBufferString(commentedCSVFixture)
			var dest [][]string

			err := consumer.Consume(reader, &dest)
			require.NoError(t, err)
			assertCSVRecords(t, dest)
		})
	})

	t.Run("with CSV writer Comma", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVWriterOpts(csv.Writer{Comma: ';'}))

		t.Run("should read comma-separated input and convert it to semicolon-separated", func(t *testing.T) {
			reader := bytes.NewBufferString(csvFixture)
			var dest bytes.Buffer

			err := consumer.Consume(reader, &dest)
			require.NoError(t, err)
			assert.EqualT(t, semiColonFixture, dest.String())
		})
	})

	t.Run("with SkipLines (streaming)", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVSkipLines(1))
		reader := bytes.NewBufferString(csvFixture)
		var dest [][]string

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)

		expected := testCSVRecords[1:]
		assert.Equalf(t, expected, dest, "expected output to skip header")
	})

	t.Run("with SkipLines (buffered)", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVSkipLines(1))
		reader := bytes.NewBufferString(csvFixture)
		var dest []byte

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)

		r := csv.NewReader(bytes.NewReader(dest))
		consumed, err := r.ReadAll()
		require.NoError(t, err)
		expected := testCSVRecords[1:]
		assert.Equalf(t, expected, consumed, "expected output to skip header")
	})

	t.Run("should detect errors on skipped lines (streaming)", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVSkipLines(1))
		reader := bytes.NewBufferString(strings.ReplaceAll(csvFixture, ",age", `,"age`))
		var dest [][]string

		err := consumer.Consume(reader, &dest)
		require.Error(t, err)
		require.ErrorContains(t, err, "record on line 1; parse error")
	})

	t.Run("should detect errors on skipped lines (buffered)", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVSkipLines(1))
		reader := bytes.NewBufferString(strings.ReplaceAll(csvFixture, ",age", `,"age`))
		var dest []byte

		err := consumer.Consume(reader, &dest)
		require.Error(t, err)
		require.ErrorContains(t, err, "record on line 1; parse error")
	})

	t.Run("with SkipLines greater than the total number of lines (streaming)", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVSkipLines(4))
		reader := bytes.NewBufferString(csvFixture)
		var dest [][]string

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)

		assert.Empty(t, dest)
	})

	t.Run("with SkipLines greater than the total number of lines (buffered)", func(t *testing.T) {
		consumer := CSVConsumer(WithCSVSkipLines(4))
		reader := bytes.NewBufferString(csvFixture)
		var dest []byte

		err := consumer.Consume(reader, &dest)
		require.NoError(t, err)

		assert.Empty(t, dest)
	})

	t.Run("with CloseStream", func(t *testing.T) {
		t.Run("wants to close stream", func(t *testing.T) {
			closingConsumer := CSVConsumer(WithCSVClosesStream())
			var dest bytes.Buffer
			r := &closingReader{b: bytes.NewBufferString(csvFixture)}

			require.NoError(t, closingConsumer.Consume(r, &dest))
			assert.EqualT(t, csvFixture, dest.String())
			assert.EqualValues(t, 1, r.calledClose)
		})

		t.Run("don't want to close stream", func(t *testing.T) {
			nonClosingConsumer := CSVConsumer()
			var dest bytes.Buffer
			r := &closingReader{b: bytes.NewBufferString(csvFixture)}

			require.NoError(t, nonClosingConsumer.Consume(r, &dest))
			assert.EqualT(t, csvFixture, dest.String())
			assert.EqualValues(t, 0, r.calledClose)
		})
	})
}

func TestCSVProducer(t *testing.T) {
	producer := CSVProducer()

	t.Run("can produce CSV from *csv.Reader", func(t *testing.T) {
		writer := new(bytes.Buffer)
		buf := bytes.NewBufferString(csvFixture)
		data := csv.NewReader(buf)

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from CSVReader", func(t *testing.T) {
		writer := new(bytes.Buffer)
		data := &csvRecordsWriter{
			records: testCSVRecords,
		}

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from Reader", func(t *testing.T) {
		writer := new(bytes.Buffer)
		data := bytes.NewReader([]byte(csvFixture))

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from WriterTo", func(t *testing.T) {
		writer := new(bytes.Buffer)
		buf := bytes.NewBufferString(csvFixture)
		data := &writerToDummy{
			b: *buf,
		}

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from BinaryMarshaler", func(t *testing.T) {
		writer := new(bytes.Buffer)
		data := &binaryMarshalDummy{str: csvFixture}

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from [][]string", func(t *testing.T) {
		writer := new(bytes.Buffer)
		data := testCSVRecords

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from alias to [][]string", func(t *testing.T) {
		writer := new(bytes.Buffer)
		type records [][]string
		data := records(testCSVRecords)

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.String())
	})

	t.Run("can produce CSV from []byte", func(t *testing.T) {
		writer := httptest.NewRecorder()
		data := []byte(csvFixture)

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.Body.String())
	})

	t.Run("can produce CSV from alias to []byte", func(t *testing.T) {
		writer := httptest.NewRecorder()
		type buffer []byte
		data := buffer(csvFixture)

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.Body.String())
	})

	t.Run("can produce CSV from string", func(t *testing.T) {
		writer := httptest.NewRecorder()
		data := csvFixture

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.Body.String())
	})

	t.Run("can produce CSV from alias to string", func(t *testing.T) {
		writer := httptest.NewRecorder()
		type buffer string
		data := buffer(csvFixture)

		err := producer.Produce(writer, data)
		require.NoError(t, err)
		assert.EqualT(t, csvFixture, writer.Body.String())
	})

	t.Run("always close data reader whenever possible", func(t *testing.T) {
		nonClosingProducer := CSVProducer()
		r := &closingWriter{}
		data := &closingReader{b: bytes.NewBufferString(csvFixture)}

		require.NoError(t, nonClosingProducer.Produce(r, data))
		assert.EqualT(t, csvFixture, r.String())
		assert.EqualValuesf(t, 0, r.calledClose, "expected the input reader NOT to be closed")
		assert.EqualValuesf(t, 1, data.calledClose, "expected the data reader to be closed")
	})

	t.Run("error cases", func(t *testing.T) {
		t.Run("unsupported type", func(t *testing.T) {
			writer := httptest.NewRecorder()
			var data struct{}

			err := producer.Produce(writer, data)
			require.Error(t, err)
		})

		t.Run("data cannot be nil", func(t *testing.T) {
			writer := httptest.NewRecorder()

			err := producer.Produce(writer, nil)
			require.Error(t, err)
		})

		t.Run("writer cannot be nil", func(t *testing.T) {
			data := []byte(csvFixture)

			err := producer.Produce(nil, data)
			require.Error(t, err)
		})

		t.Run("should propagate error from BinaryMarshaler", func(t *testing.T) {
			var rdr bytes.Buffer
			data := new(binaryMarshalDummy)

			err := producer.Produce(&rdr, data)
			require.Error(t, err)
			require.ErrorContains(t, err, "no text set")
		})
	})
}

func TestCSVProducerWithOptions(t *testing.T) {
	t.Run("with CloseStream", func(t *testing.T) {
		t.Run("wants to close stream", func(t *testing.T) {
			closingProducer := CSVProducer(WithCSVClosesStream())
			r := &closingWriter{}
			data := bytes.NewBufferString(csvFixture)

			require.NoError(t, closingProducer.Produce(r, data))
			assert.EqualT(t, csvFixture, r.String())
			assert.EqualValues(t, 1, r.calledClose)
		})

		t.Run("don't want to close stream", func(t *testing.T) {
			nonClosingProducer := CSVProducer()
			r := &closingWriter{}
			data := bytes.NewBufferString(csvFixture)

			require.NoError(t, nonClosingProducer.Produce(r, data))
			assert.EqualT(t, csvFixture, r.String())
			assert.EqualValues(t, 0, r.calledClose)
		})
	})
}

func assertCSVRecords(t testing.TB, dest [][]string) {
	assert.Len(t, dest, 3)
	for i, record := range dest {
		assert.Equal(t, testCSVRecords[i], record)
	}
}

type csvEmptyReader struct{}

func (r *csvEmptyReader) Read(_ []byte) (int, error) {
	return 0, io.EOF
}

type readerFromDummy struct {
	err error

	b bytes.Buffer
}

func (r *readerFromDummy) ReadFrom(rdr io.Reader) (int64, error) {
	if r.err != nil {
		return 0, r.err
	}

	return r.b.ReadFrom(rdr)
}

type writerToDummy struct {
	b bytes.Buffer
}

func (w *writerToDummy) WriteTo(writer io.Writer) (int64, error) {
	return w.b.WriteTo(writer)
}

type csvWriterDummy struct {
	*csv.Writer

	err error
}

func (w *csvWriterDummy) Write(record []string) error {
	if w.err != nil {
		return w.err
	}

	return w.Writer.Write(record)
}

func (w *csvWriterDummy) Error() error {
	if w.err != nil {
		return w.err
	}

	return w.Writer.Error()
}
