// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package ssh

import (
	"context"
	"fmt"
	"io"
	"math"
	"testing"
	"time"

	"github.com/hashicorp/boundary/globals"
	"github.com/hashicorp/boundary/internal/bsr"
	"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
	"github.com/hashicorp/boundary/internal/bsr/kms"
	"github.com/hashicorp/boundary/internal/db"
	"github.com/hashicorp/boundary/internal/storage"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// Note, we cannot validate the allocation of the rolling buffer
// because these tests are utilizing MemFiles and not SyncingFiles.
// MemFile does not implement a rolling buffer.
// This test validates that the minimum buffer size for each file
// type is accurate.
func TestValidateBufferSize(t *testing.T) {
	protocol := bsr.TestRegisterSummaryAllocFunc(t)

	ctx := context.Background()
	ts := time.Date(1995, time.March, 3, 12, 12, 12, 999999999, time.UTC)

	sessionId, err := db.NewPublicId(ctx, globals.SessionRecordingPrefix)
	require.NoError(t, err)

	connectionId, err := db.NewPublicId(ctx, globals.ConnectionRecordingPrefix)
	require.NoError(t, err)

	channelId, err := db.NewPublicId(ctx, globals.ChannelRecordingPrefix)
	require.NoError(t, err)

	containerName := fmt.Sprintf("%s.bsr", sessionId)
	connectionName := fmt.Sprintf("%s.connection", connectionId)
	channelName := fmt.Sprintf("%s.channel", channelId)

	var writer storage.Writer
	fs := &fstest.MemFS{}
	keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), sessionId)
	require.NoError(t, err)

	// setup session container
	session, err := bsr.NewSession(ctx, bsr.TestSessionRecordingMeta(sessionId, protocol), bsr.TestSessionMeta(sessionId), fs, keys, bsr.WithSupportsMultiplex(true))
	require.NoError(t, err)
	require.NoError(t, session.EncodeSummary(ctx, &bsr.BaseSessionSummary{
		Id:              sessionId,
		ConnectionCount: math.MaxUint64,
		StartTime:       ts,
		EndTime:         ts,
		Errors: bsr.SummaryError{
			Message: "",
		},
	}))

	// setup connection container
	connection, err := session.NewConnection(ctx, &bsr.ConnectionRecordingMeta{
		Id: connectionId,
	})
	require.NoError(t, err)
	require.NoError(t, connection.EncodeSummary(ctx, &bsr.BaseConnectionSummary{
		Id:           connectionId,
		ChannelCount: math.MaxUint64,
		BytesUp:      math.MaxUint64,
		BytesDown:    math.MaxUint64,
		StartTime:    ts,
		EndTime:      ts,
		Errors: bsr.SummaryError{
			Message: "",
		},
	}))
	// create connection inbound request writer
	writer, err = connection.NewRequestsWriter(ctx, bsr.Inbound)
	require.NoError(t, err)
	closer, ok := writer.(io.Closer)
	require.True(t, ok)
	require.NoError(t, closer.Close())
	// create connection outbound request writer
	writer, err = connection.NewRequestsWriter(ctx, bsr.Outbound)
	require.NoError(t, err)
	closer, ok = writer.(io.Closer)
	require.True(t, ok)
	require.NoError(t, closer.Close())

	// create channel container
	channel, err := connection.NewChannel(ctx, &bsr.ChannelRecordingMeta{
		Id:   channelId,
		Type: "session",
	})
	require.NoError(t, err)
	require.NoError(t, channel.EncodeSummary(ctx, &ChannelSummary{
		ChannelSummary: &bsr.BaseChannelSummary{
			Id:                    channelId,
			ConnectionRecordingId: connectionId,
			BytesUp:               math.MaxUint64,
			BytesDown:             math.MaxUint64,
			StartTime:             ts,
			EndTime:               ts,
			ChannelType:           "session",
		},
		SessionProgram:        NotApplicable,
		SubsystemName:         "",
		ExecProgram:           ExecApplicationProgramNotApplicable,
		FileTransferDirection: FileTransferNotApplicable,
		OpenFailure: &OpenChannelError{
			Reason:  math.MaxUint32,
			Message: "",
		},
	}))
	// create channel inbound request writer
	writer, err = channel.NewRequestsWriter(ctx, bsr.Inbound)
	require.NoError(t, err)
	closer, ok = writer.(io.Closer)
	require.True(t, ok)
	require.NoError(t, closer.Close())
	// create channel outbound request writer
	writer, err = channel.NewRequestsWriter(ctx, bsr.Outbound)
	require.NoError(t, err)
	closer, ok = writer.(io.Closer)
	require.True(t, ok)
	require.NoError(t, closer.Close())
	// create channel inbound message writer
	writer, err = channel.NewMessagesWriter(ctx, bsr.Inbound)
	require.NoError(t, err)
	closer, ok = writer.(io.Closer)
	require.True(t, ok)
	require.NoError(t, closer.Close())
	// create channel outbound message writer
	writer, err = channel.NewMessagesWriter(ctx, bsr.Outbound)
	require.NoError(t, err)
	closer, ok = writer.(io.Closer)
	require.True(t, ok)
	require.NoError(t, closer.Close())

	// close containers
	require.NoError(t, channel.Close(ctx))
	require.NoError(t, connection.Close(ctx))
	require.NoError(t, session.Close(ctx))

	// validate CHECKSUM.sig buffer size
	// this should always be 88 bytes for
	// all container types
	actualChecksumSignatureSize := fs.Containers[containerName].Files["SHA256SUM.sig"].Buf.Len()
	expectedChecksumSignatureBufferSize, err := bsr.SessionContainer.ChecksumSignatureBufferSize()
	require.NoError(t, err)
	assert.EqualValues(t, expectedChecksumSignatureBufferSize, actualChecksumSignatureSize)

	// validate CHECKSUM buffer size
	// this should always be 320 bytes
	// for all container types. This
	// ensures a single line can be written:
	// hash + empty space + file name
	expectedChecksumBufferSize, err := bsr.SessionContainer.ChecksumBufferSize()
	require.NoError(t, err)
	assert.EqualValues(t, expectedChecksumBufferSize, len("dc8ce2c42553ce510197c99efe21d89d6229feb4b49170511f49965f2e3cf1a3 ")+255)

	cases := []struct {
		name                       string
		containerType              bsr.ContainerType
		actualJournalSize          int
		actualRecordingMetaSize    int
		actualRecordingSummarySize int
	}{
		{
			name:                       "session",
			containerType:              bsr.SessionContainer,
			actualJournalSize:          fs.Containers[containerName].Files[".journal"].Buf.Len(),
			actualRecordingMetaSize:    len(fmt.Sprintf("connection: %s.connection", connectionId)),
			actualRecordingSummarySize: fs.Containers[containerName].Files["session-recording-summary.json"].Buf.Len(),
		},
		{
			name:                       "connection",
			containerType:              bsr.ConnectionContainer,
			actualJournalSize:          fs.Containers[containerName].Sub[connectionName].Files[".journal"].Buf.Len(),
			actualRecordingMetaSize:    len(fmt.Sprintf("channel: %s.channel", channelId)),
			actualRecordingSummarySize: fs.Containers[containerName].Sub[connectionName].Files["connection-recording-summary.json"].Buf.Len(),
		},
		{
			name:                       "channel",
			containerType:              bsr.ChannelContainer,
			actualJournalSize:          fs.Containers[containerName].Sub[connectionName].Sub[channelName].Files[".journal"].Buf.Len(),
			actualRecordingMetaSize:    fs.Containers[containerName].Sub[connectionName].Sub[channelName].Files["channel-recording.meta"].Buf.Len(),
			actualRecordingSummarySize: fs.Containers[containerName].Sub[connectionName].Sub[channelName].Files["channel-recording-summary.json"].Buf.Len(),
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			expectedJournalBufferSize, err := tc.containerType.JournalBufferSize()
			require.NoError(t, err)
			assert.EqualValues(t, expectedJournalBufferSize, tc.actualJournalSize)

			expectedRecordingMetaBufferSize, err := tc.containerType.RecordingMetaBufferSize()
			require.NoError(t, err)
			assert.EqualValues(t, expectedRecordingMetaBufferSize, tc.actualRecordingMetaSize)

			expectedRecordingSummaryBufferSize, err := tc.containerType.RecordingSummaryBufferSize()
			require.NoError(t, err)
			assert.EqualValues(t, expectedRecordingSummaryBufferSize, tc.actualRecordingSummarySize)
		})
	}
}
