package aws

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/aws/smithy-go"

	"security-center-backend/internal/modules/caasm/collector"
	caasmmodel "security-center-backend/internal/modules/caasm/model"
)

type fakeS3APIError struct {
	code    string
	message string
	fault   smithy.ErrorFault
}

func (e fakeS3APIError) Error() string {
	if e.message != "" {
		return e.message
	}
	return e.code
}

func (e fakeS3APIError) ErrorCode() string {
	return e.code
}

func (e fakeS3APIError) ErrorMessage() string {
	return e.message
}

func (e fakeS3APIError) ErrorFault() smithy.ErrorFault {
	return e.fault
}

func TestClassifyS3Exposure(t *testing.T) {
	tests := []struct {
		name   string
		pab    s3ExposureSignal
		acl    s3ExposureSignal
		policy s3ExposureSignal
		want   string
	}{
		{
			name: "public_acl_wins",
			pab:  s3ExposureSignal{State: s3ExposureStatePrivate, Reason: "public_access_block_all_enabled"},
			acl:  s3ExposureSignal{State: s3ExposureStatePublic, Reason: "acl_public_grant"},
			policy: s3ExposureSignal{
				State:  s3ExposureStatePrivate,
				Reason: "bucket_policy_not_public",
			},
			want: s3ExposureStatePublic,
		},
		{
			name: "permission_limited_beats_unknown_when_not_public",
			pab:  s3ExposureSignal{State: s3ExposureStatePrivate, Reason: "public_access_block_all_enabled"},
			acl:  s3ExposureSignal{State: s3ExposureStatePermissionLimited, Reason: "get_bucket_acl_failed"},
			policy: s3ExposureSignal{
				State:  s3ExposureStateUnknown,
				Reason: "bucket_policy_status_missing",
			},
			want: s3ExposureStatePermissionLimited,
		},
		{
			name: "unknown_when_signal_missing",
			pab:  s3ExposureSignal{State: s3ExposureStateUnknown, Reason: "public_access_block_not_configured"},
			acl:  s3ExposureSignal{State: s3ExposureStatePrivate, Reason: "acl_not_public"},
			policy: s3ExposureSignal{
				State:  s3ExposureStatePrivate,
				Reason: "bucket_policy_not_public",
			},
			want: s3ExposureStateUnknown,
		},
		{
			name: "private_when_all_known_and_non_public",
			pab:  s3ExposureSignal{State: s3ExposureStatePrivate, Reason: "public_access_block_all_enabled"},
			acl:  s3ExposureSignal{State: s3ExposureStatePrivate, Reason: "acl_not_public"},
			policy: s3ExposureSignal{
				State:  s3ExposureStatePrivate,
				Reason: "bucket_policy_not_public",
			},
			want: s3ExposureStatePrivate,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := classifyS3Exposure(tt.pab, tt.acl, tt.policy)
			if got.State != tt.want {
				t.Fatalf("classifyS3Exposure() state = %q, want %q", got.State, tt.want)
			}
			if (got.State == s3ExposureStatePublic) != got.IsPublic {
				t.Fatalf("classifyS3Exposure() isPublic mismatch for state %q", got.State)
			}
		})
	}
}

func TestClassifyS3ReadError(t *testing.T) {
	tests := []struct {
		name string
		err  error
		want string
	}{
		{
			name: "access_denied_is_permission_limited",
			err:  fakeS3APIError{code: "AccessDenied", message: "denied"},
			want: s3ExposureStatePermissionLimited,
		},
		{
			name: "forbidden_is_permission_limited",
			err:  fakeS3APIError{code: "Forbidden", message: "forbidden"},
			want: s3ExposureStatePermissionLimited,
		},
		{
			name: "generic_api_error_is_unknown",
			err:  fakeS3APIError{code: "InternalError", message: "boom"},
			want: s3ExposureStateUnknown,
		},
		{
			name: "non_api_error_is_unknown",
			err:  errors.New("plain error"),
			want: s3ExposureStateUnknown,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := classifyS3ReadError(tt.err); got != tt.want {
				t.Fatalf("classifyS3ReadError() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestSortS3BucketsOrdersByName(t *testing.T) {
	buckets := []types.Bucket{
		{Name: aws.String("z-last")},
		{Name: aws.String("A-first")},
		{Name: aws.String("m-middle")},
	}

	sorted := sortS3Buckets(buckets)
	if got := *sorted[0].Name; got != "A-first" {
		t.Fatalf("first bucket = %q, want A-first", got)
	}
	if got := *sorted[1].Name; got != "m-middle" {
		t.Fatalf("second bucket = %q, want m-middle", got)
	}
	if got := *sorted[2].Name; got != "z-last" {
		t.Fatalf("third bucket = %q, want z-last", got)
	}
}

func TestBuildS3BucketObservationIncludesApiAndWebsiteEndpoints(t *testing.T) {
	asset := caasmmodel.Asset{NativeID: "example-bucket"}
	now := time.Date(2026, 4, 10, 12, 0, 0, 0, time.UTC)

	observation := buildS3BucketObservation(asset, "eu-west-1", "example-bucket.s3-website.eu-west-1.amazonaws.com", true, now)
	if observation == nil {
		t.Fatal("observation is nil")
	}
	if got := len(observation.DomainAssets); got != 2 {
		t.Fatalf("domain count = %d, want 2", got)
	}
	if got := len(observation.Ports); got != 2 {
		t.Fatalf("port count = %d, want 2", got)
	}
	if got := observation.DomainAssets[0].DomainName; got != "example-bucket.s3.eu-west-1.amazonaws.com" {
		t.Fatalf("api domain = %q, want api endpoint", got)
	}
	if got := observation.DomainAssets[1].DomainName; got != "example-bucket.s3-website.eu-west-1.amazonaws.com" {
		t.Fatalf("website domain = %q, want website endpoint", got)
	}
	if got := observation.Ports[0].Port; got != 443 {
		t.Fatalf("first port = %d, want 443", got)
	}
	if got := observation.Ports[1].Port; got != 80 {
		t.Fatalf("second port = %d, want 80", got)
	}
	if got := observation.Ports[0].Exposure; got != "public" {
		t.Fatalf("https exposure = %q, want public", got)
	}
}

func TestNormalizeS3BucketRegion(t *testing.T) {
	tests := []struct {
		name   string
		input  string
		output string
	}{
		{name: "legacy eu alias", input: "EU", output: "eu-west-1"},
		{name: "lowercases region", input: "Us-East-1", output: "us-east-1"},
		{name: "preserves unknown marker", input: "unknown", output: "unknown"},
		{name: "blank stays blank", input: "", output: ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := normalizeS3BucketRegion(tt.input); got != tt.output {
				t.Fatalf("normalizeS3BucketRegion(%q) = %q, want %q", tt.input, got, tt.output)
			}
		})
	}
}

func TestBucketMetadataS3ConfigUsesBucketRegion(t *testing.T) {
	collector := &AWSCollector{cfg: aws.Config{Region: "us-east-1"}}

	if got := collector.bucketMetadataS3Config("eu-west-1").Region; got != "eu-west-1" {
		t.Fatalf("bucketMetadataS3Config() region = %q, want eu-west-1", got)
	}
	if got := collector.bucketMetadataS3Config("EU").Region; got != "eu-west-1" {
		t.Fatalf("bucketMetadataS3Config() legacy EU region = %q, want eu-west-1", got)
	}
	if got := collector.bucketMetadataS3Config("unknown").Region; got != "us-east-1" {
		t.Fatalf("bucketMetadataS3Config() unknown region = %q, want us-east-1 fallback", got)
	}
}

func TestS3FinalizeChunkEmitsForEmptyInventory(t *testing.T) {
	sink := &recordingChunkSink{}
	summary := &collector.CollectionSummary{CollectedAt: time.Date(2026, 4, 10, 12, 0, 0, 0, time.UTC)}

	if err := emitS3FinalizeChunk(context.Background(), sink, summary, 1); err != nil {
		t.Fatalf("emitS3FinalizeChunk() error = %v", err)
	}
	if len(sink.chunks) != 1 {
		t.Fatalf("captured chunks = %d, want 1", len(sink.chunks))
	}
	chunk := sink.chunks[0]
	if chunk.Target != "s3/finalize" {
		t.Fatalf("target = %q, want s3/finalize", chunk.Target)
	}
	if chunk.Sequence != 1 {
		t.Fatalf("sequence = %d, want 1", chunk.Sequence)
	}
	if chunk.Checkpoint.GlobalService != "s3" || !chunk.Checkpoint.Eligible {
		t.Fatalf("checkpoint = %+v, want eligible s3 checkpoint", chunk.Checkpoint)
	}
	if chunk.Metadata["finalize_only"] != true {
		t.Fatalf("finalize_only metadata = %v, want true", chunk.Metadata["finalize_only"])
	}
}
