import type { UserConfig } from '@bamboocss/types'
import { describe, expect, test } from 'vitest'
import { validateConfig } from '../src/validate-config'

describe('validateConfig', () => {
  test('should not throw when no errors', () => {
    const config: Partial<UserConfig> = {
      theme: {
        breakpoints: {
          sm: '640px',
          md: '768px',
          lg: '1024px',
        },
        tokens: {
          colors: {
            primary: { value: '#000', type: 'color', description: 'The primary color' },
            secondary: { value: '#fff' },
            tertiary: {
              value: '#ddd',
              type: 'color',
              description: 'gray.100',
              extensions: {
                category: 'colors',
                type: 'primitive',
              },
            },
          },
          fontSizes: {
            sm: { value: '12px' },
            md: { value: '16px' },
            lg: { value: '20px' },
          },
        },
        semanticTokens: {
          colors: {
            bg: { value: 'token(colors.primary)' },
          },
          fontSizes: {
            main: { value: 'token(fontSizes.md)' },
          },
        },
      },
      conditions: {
        dark: '@media (prefers-color-scheme: dark)',
        pinkTheme: '&[data-theme="pink"]',
      },
      patterns: {
        'btn-primary': {},
      },
    }

    expect(() => validateConfig(config)).not.toThrow()
  })

  test('should throw with validation: error', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        breakpoints: {
          sm: '640px',
          md: '768px',
          lg: '1024px',
        },
        tokens: {
          colors: {
            primary: { value: '#000' },
            // @ts-expect-error should be an object with { value }
            secondary: '#fff',
          },
          fontSizes: {
            sm: { value: '12px' },
            md: { value: '16px' },
            lg: { value: '20px' },
          },
        },
        semanticTokens: {
          colors: {
            bg: { value: 'token(colors.primary)' },
          },
          fontSizes: {
            main: { value: 'token(fontSizes.md)' },
          },
        },
      },
      conditions: {
        dark: '@media (prefers-color-scheme: dark)',
        pinkTheme: '&[data-theme="pink"]',
      },
      patterns: {
        'btn-primary': {},
      },
    }

    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Token must contain 'value': \`theme.tokens.colors.secondary\`
      ]
    `,
    )
  })

  test('should not throw with validation: warn', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        breakpoints: {
          sm: '640px',
          md: '768px',
          lg: '1024px',
        },
        tokens: {
          colors: {
            primary: { value: '#000' },
            // @ts-expect-error should be an object with { value }
            secondary: '#fff',
          },
          fontSizes: {
            sm: { value: '12px' },
            md: { value: '16px' },
            lg: { value: '20px' },
          },
        },
        semanticTokens: {
          colors: {
            bg: { value: 'token(colors.primary)' },
          },
          fontSizes: {
            main: { value: 'token(fontSizes.md)' },
          },
        },
      },
      conditions: {
        dark: '@media (prefers-color-scheme: dark)',
        pinkTheme: '&[data-theme="pink"]',
      },
      patterns: {
        'btn-primary': {},
      },
    }

    expect(() => validateConfig(config)).not.toThrow()
  })

  test('should report errors on validation warn - altogether', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        breakpoints: {
          sm: '640em', // invalid unit
          md: '768px',
          lg: '1024px',
        },
        tokens: {
          colors: {
            primary: { value: '#000' },
            // @ts-expect-error should be an object with { value }
            secondary: '#fff',
            group: {
              with: {
                nested: {
                  value: 'red',
                },
                // @ts-expect-error should be an object with { value }
                invalid: 'blue',
              },
            },
          },
          fontSizes: {
            sm: { value: '12px' },
            md: { value: '16px' },
            lg: { value: '20px' },
          },
        },
        semanticTokens: {
          colors: {
            bg: { value: 'token(colors.secondary)' },
            primary: { value: 'token(colors.bg)' }, // clashing name (already exists in tokens)
            another: {
              group: {
                ok: {
                  value: 'token(colors.group.with.nested)',
                },
                stillok: {
                  value: {
                    base: 'blue',
                    _hover: 'green',
                    _active: 'token(colors.doesntexist)', // invalid missing reference
                  },
                },
                // @ts-expect-error  should be an object with { value }
                invalid: 'token(colors.group.with.invalid)',
                recursive: {
                  value: 'token(colors.another.circular)', // invalid circular reference
                },
                missing: {
                  value: 'token(colors.missing)', // invalid missing reference
                },
              },
              self: {
                value: 'token(colors.another.self)', // invalid self reference
              },
              circular: {
                value: 'token(colors.another.group.recursive)', // invalid circular reference
              },
            },
          },
          fontSizes: {
            main: { value: 'token(fontSizes.md2)' }, // invalid reference
          },
        },
        recipes: {
          stack: {
            // clashing name (already exists in patterns)
            className: 'stack',
          },
        },
        slotRecipes: {
          'btn-primary': {
            // clashing name (already exists in patterns)
            className: 'btn-primary',
            slots: [],
          },
        },
      },
      conditions: {
        dark: '@media (prefers-color-scheme: dark)',
        pinkTheme: '[data-theme="pink"]', // invalid selector (must contain '&')
      },
      patterns: {
        'btn-primary': {},
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`
      Set {
        "[breakpoints] All breakpoints must use the same unit: \`640em, 768px, 1024px\`",
        "[conditions] Selectors should contain the \`&\` character: \`[data-theme="pink"]\`",
        "[tokens] Token must contain 'value': \`theme.tokens.colors.secondary\`",
        "[tokens] Token must contain 'value': \`theme.tokens.colors.group.with.invalid\`",
        "[tokens] Token must contain 'value': \`theme.semanticTokens.colors.another.group.invalid\`",
        "[tokens] Missing token: \`colors.doesntexist\` used in \`theme.semanticTokens.colors.another.group.stillok\`",
        "[tokens] Circular token reference: \`colors.another.group.recursive\` -> \`colors.another.circular\` -> ... -> \`colors.another.group.recursive\`",
        "[tokens] Missing token: \`colors.missing\` used in \`theme.semanticTokens.colors.another.group.missing\`",
        "[tokens] Self token reference: \`colors.another.self\`",
        "[tokens] Circular token reference: \`colors.another.self\` -> \`colors.another.self\` -> ... -> \`colors.another.self\`",
        "[tokens] Circular token reference: \`colors.another.circular\` -> \`colors.another.group.recursive\` -> ... -> \`colors.another.circular\`",
        "[tokens] Missing token: \`fontSizes.md2\` used in \`theme.semanticTokens.fontSizes.main\`",
        "[recipes] This recipe name is already used in \`patterns\`: btn-primary",
      }
    `)
  })

  test('should report error if breakpoints use different units', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        breakpoints: {
          sm: '640em', // invalid unit
          md: '768px',
          lg: '1024px',
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [breakpoints] All breakpoints must use the same unit: \`640em, 768px, 1024px\`
      ]
    `,
    )
  })

  test('should report error for condition selectors without & character', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      conditions: {
        pinkTheme: '[data-theme="pink"]', // invalid selector
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [conditions] Selectors should contain the \`&\` character: \`[data-theme="pink"]\`
      ]
    `,
    )
  })

  test('should accept a valid object (multi-block) condition', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      conditions: {
        hoverActive: {
          '@media (hover: hover)': { '&:is(:hover, [data-hover])': '@slot' },
          '@media (hover: none)': { '&:is(:active, [data-active])': '@slot' },
        },
      },
    }
    expect(() => validateConfig(config)).not.toThrow()
  })

  test('should report error when an object condition has no @slot marker', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      conditions: {
        hoverActive: {
          '@media (hover: hover)': { '&:hover': {} },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [conditions] Object conditions must contain at least one \`'@slot'\` marker
      ]
    `,
    )
  })

  test('should report error when a leaf is a non-@slot string (typo)', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      conditions: {
        // @ts-expect-error intentional: typo on slot marker
        hoverActive: {
          '@media (hover: hover)': { '&:hover': '@stol' },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [conditions] Object condition leaves must be the literal string \`'@slot'\`, got \`"@stol"\` at \`&:hover\`
      - [conditions] Object conditions must contain at least one \`'@slot'\` marker
      ]
    `,
    )
  })

  test('should report error for token paths not ending with value', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        tokens: {
          colors: {
            // @ts-expect-error should be an object with { value }
            secondary: '#fff',
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Token must contain 'value': \`theme.tokens.colors.secondary\`
      ]
    `,
    )
  })

  test('should report error for semantic token paths not containing value', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        semanticTokens: {
          colors: {
            // @ts-expect-error should be an object with { value }
            invalid: 'token(colors.group.with.invalid)',
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Token must contain 'value': \`theme.semanticTokens.colors.invalid\`
      ]
    `,
    )
  })

  test('should report error for missing token references', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        semanticTokens: {
          colors: {
            another: {
              group: {
                stillok: {
                  value: {
                    _active: 'token(colors.doesntexist)', // invalid missing reference
                  },
                },
              },
            },
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Missing token: \`colors.doesntexist\` used in \`theme.semanticTokens.colors.another.group.stillok\`
      ]
    `,
    )
  })

  test('should report error for circular token references', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        semanticTokens: {
          colors: {
            another: {
              circular: {
                value: 'token(colors.another.group.recursive)',
              },
              group: {
                recursive: {
                  value: 'token(colors.another.circular)',
                },
              },
            },
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Circular token reference: \`colors.another.circular\` -> \`colors.another.group.recursive\` -> ... -> \`colors.another.circular\`
      - [tokens] Circular token reference: \`colors.another.group.recursive\` -> \`colors.another.circular\` -> ... -> \`colors.another.group.recursive\`
      ]
    `,
    )
  })

  test('should report error for self token references', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        semanticTokens: {
          colors: {
            another: {
              self: {
                value: 'token(colors.another.self)', // invalid self reference
              },
            },
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Self token reference: \`colors.another.self\`
      - [tokens] Circular token reference: \`colors.another.self\` -> \`colors.another.self\` -> ... -> \`colors.another.self\`
      ]
    `,
    )
  })

  test('should report error for clashing token names', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        tokens: {
          colors: {
            primary: { value: '#000' },
          },
        },
        semanticTokens: {
          colors: {
            primary: { value: 'token(colors.bg)' }, // clashing name
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Missing token: \`colors.bg\` used in \`theme.semanticTokens.colors.primary\`
      ]
    `,
    )
  })

  test('should report error for clashing recipe names', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        recipes: {
          stack: {
            className: 'stack',
          },
        },
      },
      patterns: {
        stack: {}, // clashing name
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [recipes] This recipe name is already used in \`patterns\`: \`stack\`
      ]
    `,
    )
  })

  test('simple token example', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        tokens: {
          colors: {
            //@ts-expect-error
            primary: 'red',
            secondary: { value: '#fff', description: 'welcome', extensions: { something: 'Alex' } },
            bbb: { value: 'blue' },
          },
        },
      },
    }
    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Token must contain 'value': \`theme.tokens.colors.primary\`
      ]
    `,
    )
  })

  test('simple semantic token example', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        semanticTokens: {
          colors: {
            // @ts-expect-error
            primary: 'token(colors.bbb)',
            secondary: { value: '#fff', description: 'welcome', extensions: { something: 'Alex' } },
            bbb: { value: 'token(colors.primary)' },
            // valid: { value: { base: 'red', _dark: 'blue' }, extensions: { something: 'Alex' } },
            group: {
              nested: {
                something: {
                  value: { base: 'token(colors.red)', _dark: 'blue' },
                },
              },
            },
          },
        },
      },
    }

    expect(() => validateConfig(config)).toThrowErrorMatchingInlineSnapshot(
      `
      [Error: ⚠️ Invalid config:
      - [tokens] Token must contain 'value': \`theme.semanticTokens.colors.primary\`
      - [tokens] Unknown token reference: \`colors.primary\` used in \`token(colors.bbb)\`
      - [tokens] Missing token: \`colors.red\` used in \`theme.semanticTokens.colors.group.nested.something\`
      ]
    `,
    )
  })

  // https://github.com/gajus/bamboocss/issues/2283
  test('"missing token" warning when using nested tokens', () => {
    const config: Partial<UserConfig> = {
      validation: 'error',
      theme: {
        semanticTokens: {
          colors: {
            primary: {
              DEFAULT: { value: '#ff3333' },
              lighter: { value: '#ff6666' },
            },
            background: { value: 'token(colors.primary)' }, // <-- ⚠️ wrong warning
            background2: { value: 'token(colors.primary.lighter)' }, // <-- no warning, correct
          },
        },
      },
    }

    expect(() => validateConfig(config)).not.toThrow()
  })

  // https://github.com/gajus/bamboocss/issues/2284
  test('warn about nesting in "value" twice', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          colors: {
            red: {
              300: { value: 'red' },
            },
          },
        },
        semanticTokens: {
          colors: {
            primary: {
              // should probably not wrap twice in value
              value: { value: 'token(colors.red.300)' },
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`
      Set {
        "[tokens] You used \`value\` twice resulting in an invalid token \`theme.tokens.colors.primary.value.value\`",
      }
    `)
  })

  test('prevent space in token keys', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          fonts: {
            'Press Start 1P': { value: 'Press Start 1P' },
            PressStart2P: { value: 'Press Start 2P' },
          },
        },
        semanticTokens: {
          fonts: {
            'Press Start 3P': { value: 'Press Start 3P' },
            PressStart4P: { value: 'Press Start 4P' },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`
      Set {
        "[tokens] Token key must not contain spaces: \`theme.tokens.fonts.Press Start 1P\`",
        "[tokens] Token key must not contain spaces: \`theme.tokens.fonts.Press Start 3P\`",
      }
    `)
  })

  test('referencing a token with DEFAULT from a DEFAULT', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          colors: {
            primary: {
              DEFAULT: { value: 'blue' },
              300: { value: 'red' },
            },
          },
        },
        semanticTokens: {
          colors: {
            button: {
              primary: {
                bg: {
                  DEFAULT: {
                    value: {
                      base: 'token(colors.primary)',
                      _dark: 'white',
                    },
                  },
                },
              },
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`undefined`)
  })

  test('using color opacity modifier on known color shouldnt throw', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      conditions: {
        light: '.light &',
        dark: '.dark &',
      },
      theme: {
        tokens: {
          colors: {
            blue: { 500: { value: 'blue' } },
            green: { 500: { value: 'green' } },
          },
          opacity: {
            half: { value: 0.5 },
          },
        },
        semanticTokens: {
          colors: {
            secondary: {
              value: {
                base: 'red',
                _light: 'token(colors.blue.500/32)', // <-- wasn't working as expected
                _dark: 'token(colors.green.500/half)',
              },
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`undefined`)
  })

  test('should validate shadow tokens', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          shadows: {
            strong: {
              value: '2px 4px 16px 0px token(colors.gray.934/16), 0px 2px 4px 0px token(colors.gray.34/8)',
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`
      Set {
        "[tokens] Missing token: \`colors.gray.34\` used in \`theme.tokens.shadows.strong\`",
        "[tokens] Missing token: \`colors.gray.934\` used in \`theme.tokens.shadows.strong\`",
      }
    `)
  })

  test('should validate composite tokens', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          shadows: {
            strong: {
              value: {
                offsetX: 2,
                offsetY: 4,
                blur: 16,
                spread: 0,
                color: 'token(colors.gray.100/16)',
              },
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`
      Set {
        "[tokens] Missing token: \`colors.gray.100\` used in \`theme.tokens.shadows.strong\`",
      }
    `)
  })

  test('should validate composite array tokens', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          borders: {
            soft: {
              value: {
                width: 1,
                style: 'solid',
                color: 'token(colors.gray.100)',
              },
            },
          },
          shadows: {
            strong: {
              value: [
                {
                  offsetX: 2,
                  offsetY: 4,
                  blur: 16,
                  spread: 0,
                  color: 'token(colors.gray.100/16)',
                },
                {
                  offsetX: 0,
                  offsetY: 2,
                  blur: 4,
                  spread: 0,
                  color: 'token(colors.gray.300/8)',
                },
              ],
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`
      Set {
        "[tokens] Missing token: \`colors.gray.100\` used in \`theme.tokens.borders.soft\`",
        "[tokens] Missing token: \`colors.gray.300\` used in \`theme.tokens.shadows.strong\`",
        "[tokens] Missing token: \`colors.gray.100\` used in \`theme.tokens.shadows.strong\`",
      }
    `)
  })

  test('should work with deprecated', () => {
    const config: Partial<UserConfig> = {
      validation: 'warn',
      theme: {
        tokens: {
          colors: {
            green: {
              400: {
                value: '#dfdf',
                deprecated: true,
              },
            },
          },
        },
      },
    }

    expect(validateConfig(config)).toMatchInlineSnapshot(`undefined`)
  })
})

/**
 * `strictTokens` is a boolean, and a string that is not one is simply *on*.
 *
 * The generated types test it for truthiness, so `strictValues: 'unknown-tokens'` — which used
 * to be a setting — turns every raw CSS value in the project into a type error, from a config
 * that reads as though it asked for something milder. A build that exits 0 and then a wall of
 * errors matching nothing in the docs.
 */
describe('strictValues', () => {
  const validate = (strictValues: unknown) =>
    validateConfig({ strictValues, validation: 'error' } as Partial<UserConfig>)

  test('accepts both settings, and absence', () => {
    for (const value of [undefined, true, false]) {
      expect(() => validate(value), String(value)).not.toThrow()
    }
  })

  test('names a spelling that is not one of them', () => {
    expect(() => validate('unknown')).toThrow(/strictValues/)
    expect(() => validate('unknown')).toThrow(/is not a setting/)
    expect(() => validate('yes')).toThrow(/is not a setting/)
  })
})
