package typescript_test

import (
	"strings"
	"testing"

	"code.justin.tv/spade/code-generator/internal"
	"code.justin.tv/spade/code-generator/internal/typescript"
	"github.com/stretchr/testify/assert"
)

var schema = `
events:
  - name: invariant_ping
    description: An event fired 2 times every second by Data Platform to canary Spade.
    fields:
      - name: origin
        description: A generic column detailing the source of this event.
        type:
          name: enum
          values:
            - internal
            - external
      - name: sequence_number
        description: A generic sequence number for sequencing multiple events
        type:
          name: long
        expectations:
          - name: values_to_not_be_null
      - name: ip
        description: The event sources IPv4 address.
        type:
          name: string
          length: 15
        expectations:
          - name: values_to_not_be_null
          - name: values_to_match_regex_list
            patterns:
              - ^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$
`

var schemaNoExpectations = `
events:
  - name: invariant_ping
    description: An event fired 2 times every second by Data Platform to canary Spade.
    fields:
      - name: origin
        description: A generic column detailing the source of this event.
        type:
          name: enum
          values:
            - internal
            - external
      - name: sequence_number
        description: A generic sequence number for sequencing multiple events
        type:
          name: long
      - name: ip
        description: The event sources IPv4 address.
        type:
          name: string
          length: 15
`

func TestGenerateCode_OK(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	internal.P(o, `const events = new TailoredSpadeEventFactory();`)
	internal.P(o, `console.log(events.invariantPing({`)
	internal.P(o, `   ip: "192.0.0.1",`)
	internal.P(o, `   origin: "internal",`)
	internal.P(o, `   sequence_number: 2,`)
	internal.P(o, `})?.validate())`)

	assert.Equal(t, "undefined", runTS(o.String(), true))
}

func TestGenerateCode_OK_OmitGenericTypeFromInput(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	// no complain about missing "common attribute" sequence number 👌
	internal.P(o, `const events = new TailoredSpadeEventFactory<{ sequence_number: string }>();`)
	internal.P(o, `const event = events.invariantPing({`)
	internal.P(o, `   ip: "192.0.0.1",`)
	internal.P(o, `   origin: "internal",`)
	internal.P(o, `})`)

	// fail on compilation error ensure code above is valid.
	runTS(o.String(), true)
}

func TestGenerateCode_FailTypeCheck(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	internal.P(o, `const events = new TailoredSpadeEventFactory();`)
	internal.P(o, `events.invariantPing({`)
	internal.P(o, `   origin: 1,`)
	internal.P(o, `   ip: "192.0.0.1",`)
	internal.P(o, `   sequence_number: 2,`)
	internal.P(o, `})`)

	assert.Contains(t, runTS(o.String(), false), `Type 'number' is not assignable to type '"internal" | "external"'`)
}

func TestGenerateCode_Expectations(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	internal.P(o, `const events = new TailoredSpadeEventFactory();`)
	internal.P(o, `console.log(events.invariantPing({`)
	internal.P(o, `   origin: "internal",`)
	internal.P(o, `   sequence_number: 2,`)
	internal.P(o, `   ip: "not-an-ip-address",`)
	internal.P(o, `})?.validate())`)

	assert.Contains(t, o.String(), "function concat")
	assert.Contains(t, o.String(), "function withPrefix")
	assert.Contains(t, o.String(), "function valuesToNotBeNull")
	assert.Contains(t, o.String(), "function valuesToMatchRegexList")
	assert.Contains(t, runTS(o.String(), false), "spade event invariant_ping failed expectations:\nip: expected to match one of /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/ got not-an-ip-address")
}

func TestGenerateCode_NoExpectations(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schemaNoExpectations))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	internal.P(o, `const events = new TailoredSpadeEventFactory();`)
	internal.P(o, `console.log(events.invariantPing({`)
	internal.P(o, `   origin: "internal",`)
	internal.P(o, `   sequence_number: 2,`)
	internal.P(o, `   ip: "not-an-ip-address",`)
	internal.P(o, `})?.validate)`)

	assert.NotContains(t, o.String(), "function concat")
	assert.NotContains(t, o.String(), "function withPrefix")
	assert.NotContains(t, o.String(), "function valuesToNotBeNull")
	assert.NotContains(t, o.String(), "function valuesToMatchRegexList")

	assert.Equal(t, "undefined", runTS(o.String(), true))
}

func TestGenerateCode_EventsUnion_OK(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	internal.P(o, `const e: SpadeEvent = {`)
	internal.P(o, `  name: "invariant_ping",`)
	internal.P(o, `  attributes: {`)
	internal.P(o, `    origin: "internal",`)
	internal.P(o, `    sequence_number: 2,`)
	internal.P(o, `    ip: "not-an-ip-address",`)
	internal.P(o, `  }`)
	internal.P(o, `}`)

	// fail on compilation error ensure code above is valid.
	runTS(o.String(), true)
}

func TestGenerateCode_EventsUnion_Invalid(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	internal.P(o, `const e: SpadeEvent = {`)
	internal.P(o, `  name: "not_defined",`)
	internal.P(o, `  attributes: {`)
	internal.P(o, `    origin: "internal",`)
	internal.P(o, `    sequence_number: 2,`)
	internal.P(o, `    ip: "not-an-ip-address",`)
	internal.P(o, `  }`)
	internal.P(o, `}`)

	assert.Contains(t, runTS(o.String(), false), `Type '"not_defined"' is not assignable to type '"invariant_ping"'`)
}

func TestGenerateCode_TailoredSpadeEvent_OK(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	// ip field passed to TailoredSpadeEvent so it is no longer required
	internal.P(o, `const e: TailoredSpadeEvent<{ip: string}> = {`)
	internal.P(o, `  name: "invariant_ping",`)
	internal.P(o, `  attributes: {`)
	internal.P(o, `    origin: "internal",`)
	internal.P(o, `    sequence_number: 2,`)
	internal.P(o, `  }`)
	internal.P(o, `}`)

	// fail on compilation error ensure code above is valid.
	runTS(o.String(), true)
}

func TestGenerateCode_TailoredSpadeEvent_Invalid(t *testing.T) {
	o := &strings.Builder{}

	schema, err := internal.GetYamlSchema("hardcode", strings.NewReader(schema))
	assert.NoError(t, err)

	err = typescript.GenerateCode(*schema, o, typescript.GenerationOptions{
		GuardExpectationsWithExpression: "true",
	})
	assert.NoError(t, err)

	// Empty interface passed to TailoredSpadeEvent
	internal.P(o, `const e: TailoredSpadeEvent<{}> = {`)
	internal.P(o, `  name: "invariant_ping",`)
	internal.P(o, `  attributes: {`)
	internal.P(o, `    origin: "internal",`)
	internal.P(o, `    sequence_number: 2,`)
	internal.P(o, `  }`)
	internal.P(o, `}`)

	assert.Contains(t, runTS(o.String(), false), `Property 'ip' is missing in type`)
}
