package m2m_test

import (
	"encoding/xml"
	"errors"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
)

// validator — общий интерфейс для каскадной валидации в тестах.
type validator interface {
	Validate() error
}

// roundTripCase описывает приёмочный кейс round-trip на эталонном файле.
type roundTripCase struct {
	path string
	mk   func() any
}

// roundTripCases — соответствие "файл -> ожидаемый Go-тип сообщения".
var roundTripCases = []roundTripCase{
	{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferRequest.xml"), func() any { return new(m2m.M2MTransferRequest) }},
	{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferDecision.xml"), func() any { return new(m2m.M2MTransferDecision) }},
	{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferResponse.xml"), func() any { return new(m2m.M2MTransferResponse) }},
	{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferHandbook.xml"), func() any { return new(m2m.M2MTransferHandbook) }},
	{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferHandbookRequest.xml"), func() any { return new(m2m.M2MTransferHandbookRequest) }},
	{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferParticipantForm.xml"), func() any { return new(m2m.M2MTransferParticipantForm) }},
	{filepath.Join("..", "..", "DOC", "Эталонные сообщения", "M2MTransferRequest_эталон.xml"), func() any { return new(m2m.M2MTransferRequest) }},
	{filepath.Join("..", "..", "DOC", "Эталонные сообщения", "M2MTransferDecision_эталон.xml"), func() any { return new(m2m.M2MTransferDecision) }},
}

// clearXMLNameSpace обнуляет поле Space у верхнеуровневого xml.Name (если
// есть), чтобы round-trip-сравнение не зависело от namespace корня входного
// документа.
func clearXMLNameSpace(v any) {
	rv := reflect.ValueOf(v)
	if rv.Kind() != reflect.Ptr || rv.IsNil() {
		return
	}
	rv = rv.Elem()
	if rv.Kind() != reflect.Struct {
		return
	}
	f := rv.FieldByName("XMLName")
	if f.IsValid() && f.CanSet() && f.Type() == reflect.TypeOf(xml.Name{}) {
		f.Set(reflect.ValueOf(xml.Name{Local: f.Interface().(xml.Name).Local}))
	}
}

func TestRoundTrip(t *testing.T) {
	for _, c := range roundTripCases {
		c := c
		t.Run(filepath.Base(c.path), func(t *testing.T) {
			b1, err := os.ReadFile(c.path)
			if err != nil {
				t.Fatalf("read %s: %v", c.path, err)
			}

			s1 := c.mk()
			if err := nsdxml.Unmarshal(b1, s1); err != nil {
				t.Fatalf("unmarshal оригинала: %v", err)
			}

			if v, ok := s1.(validator); ok {
				if err := v.Validate(); err != nil {
					t.Fatalf("Validate после первого Unmarshal: %v", err)
				}
			}

			b2, err := nsdxml.Marshal(s1)
			if err != nil {
				t.Fatalf("marshal: %v", err)
			}

			// Проверяем, что пролог windows-1251 проставлен.
			if !strings.HasPrefix(string(nsdxml.DecodeWindows1251(b2)), `<?xml version="1.0" encoding="windows-1251"?>`) {
				t.Fatalf("в выходе нет windows-1251 пролога")
			}

			s2 := c.mk()
			if err := nsdxml.Unmarshal(b2, s2); err != nil {
				t.Fatalf("unmarshal после Marshal: %v", err)
			}

			// XMLName.Space — это метаданные пространства имён входного XML,
			// а не полезная нагрузка. Часть документов НРД присылает с
			// namespace на корне (официальные примеры), часть — без (реальный
			// ответ робота МОСТ). Парсер намеренно namespace-agnostic, поэтому
			// после re-marshal корневой namespace может отличаться. Для
			// приёмочных (receive-only) документов это несущественно — сравниваем
			// без учёта XMLName.Space.
			clearXMLNameSpace(s1)
			clearXMLNameSpace(s2)
			if !reflect.DeepEqual(s1, s2) {
				t.Errorf("round-trip структуры разошлись:\nS1 = %+v\nS2 = %+v", s1, s2)
			}
		})
	}
}

func TestValidatorsPositive(t *testing.T) {
	cases := []struct {
		name string
		v    validator
	}{
		{"ReferenceId", m2m.ReferenceID("M2M2026030200001")},
		{"ISIN", m2m.ISIN("RU0007661625")},
		{"OrganizationINN", m2m.OrganizationINN("7702070139")},
		{"DeponentCode", m2m.DeponentCode("MC0079200000")},
		{"UUID", m2m.UUID("c02a1d5e-c2af-4799-bab4-953f133c5133")},
		{"SecurityCode", m2m.SecurityCode("MM0766162534")},
		{"IdentityDocSerial", m2m.IdentityDocSerial("4512")},
		{"AccountId", m2m.AccountID("31MC0021900000F01")},
		{"StatusCode", m2m.StatusInfo},
		{"IIAContractType", m2m.IIAContractT03},
		{"SecurityClassification", m2m.SecurityBond},
		{"SecurityCategory", m2m.CategoryOrdn},
		{"IdentityDocumentCode", m2m.DocCode21},
		{"IsolationStatus", m2m.IsolationSGDN},
		{"Decimal16", m2m.Decimal16("2500.75")},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if err := c.v.Validate(); err != nil {
				t.Errorf("ожидалось без ошибок, получено: %v", err)
			}
		})
	}
}

func TestValidatorsNegative(t *testing.T) {
	cases := []struct {
		name string
		v    validator
	}{
		{"ReferenceId короткий", m2m.ReferenceID("M2M123")},
		{"ReferenceId без префикса", m2m.ReferenceID("XYZ2026030200001")},
		{"ISIN короткий", m2m.ISIN("RU0007")},
		{"ISIN с lowercase", m2m.ISIN("ru0007661625")},
		{"INN короткий", m2m.OrganizationINN("123")},
		{"INN с буквами", m2m.OrganizationINN("770207013A")},
		{"DeponentCode пустой", m2m.DeponentCode("")},
		{"DeponentCode lowercase", m2m.DeponentCode("mc007920")},
		{"DeponentCode слишком длинный", m2m.DeponentCode("AAAAAAAAAAAAA")},
		{"UUID без дефисов", m2m.UUID("c02a1d5ec2af4799bab4953f133c5133aaaa")},
		{"UUID с не-hex символом", m2m.UUID("c02a1d5e-c2af-4799-babZ-953f133c5133")},
		{"UUID короткий", m2m.UUID("c02a1d5e-c2af-4799-bab4-953f133c513")},
		{"SecurityCode короткий", m2m.SecurityCode("ABC")},
		{"SecurityCode с lowercase", m2m.SecurityCode("mm0766162534")},
		{"IdentityDocSerial пустой", m2m.IdentityDocSerial("")},
		{"IdentityDocSerial с пробелом", m2m.IdentityDocSerial("45 12")},
		{"AccountId пустой", m2m.AccountID("")},
		{"AccountId длиннее 50", m2m.AccountID(strings.Repeat("A", 51))},
		{"StatusCode unknown", m2m.StatusCode("OK")},
		{"IIAContractType T01", m2m.IIAContractType("T01")},
		{"SecurityClassification unknown", m2m.SecurityClassification("STCK")},
		{"SecurityCategory unknown", m2m.SecurityCategory("XXXX")},
		{"IdentityDocumentCode 99", m2m.IdentityDocumentCode("99")},
		{"IsolationStatus FOO", m2m.IsolationStatus("FOO")},
		{"Decimal16 пустой", m2m.Decimal16("")},
		{"Decimal16 с буквами", m2m.Decimal16("12a.5")},
		{"Decimal16 слишком много дробных", m2m.Decimal16("1.12345678901234567")},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			err := c.v.Validate()
			if err == nil {
				t.Errorf("ожидалась ошибка")
				return
			}
			if !errors.Is(err, m2m.ErrInvalid) {
				t.Errorf("ожидалась ErrInvalid, получено: %v", err)
			}
		})
	}
}

func TestChoiceValidators(t *testing.T) {
	t.Run("CostInfo пустой", func(t *testing.T) {
		if err := (m2m.CostInfo{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
	t.Run("CostInfo оба поля", func(t *testing.T) {
		c := m2m.CostInfo{
			Yes: &m2m.CostInfoYes{Code: "MC0010300032"},
			No:  &m2m.CostInfoNo{},
		}
		if err := c.Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
	t.Run("Quantity пустой", func(t *testing.T) {
		if err := (m2m.Quantity{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
	t.Run("Quantity оба поля", func(t *testing.T) {
		whole := uint64(100)
		frac := m2m.Decimal16("1.5")
		q := m2m.Quantity{Whole: &whole, Fractional: &frac}
		if err := q.Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
	t.Run("SecurityDetails пустой", func(t *testing.T) {
		if err := (m2m.SecurityDetails{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
	t.Run("IdentificationDetails пустой", func(t *testing.T) {
		if err := (m2m.IdentificationDetails{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
	t.Run("DecisionTransfer пустой", func(t *testing.T) {
		if err := (m2m.DecisionTransfer{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
			t.Errorf("ожидалась ErrChoice, получено: %v", err)
		}
	})
}
